Server stuff

This commit is contained in:
Bottersnike 2022-11-17 02:03:08 +00:00
parent 84a87f5a7f
commit b8f8ac759b
10 changed files with 1069 additions and 1 deletions

@ -1 +0,0 @@
Subproject commit dec7ca3536cf459be21b7284358bf2bea4eb3d14

43
eaapi/server/README.md Normal file
View File

@ -0,0 +1,43 @@
# eaapi.server
## Quickstart
```py
server = EAMServer("http://127.0.0.1:5000")
@server.handler("message", "get")
def message(ctx):
ctx.resp.append("message", expire="300", status="0")
server.run("0.0.0.0", 5000)
```
```py
EAMServer(
# The URL this server can be access at. Used for services
public_url,
# Add `/<service>/` as a prefix when generating service urls (useful when debugging games)
prefix_services: bool = False,
# If both the URL and the query params match, which one gets the final say?
prioritise_params: bool = False,
# Include e-Amusement specific details of why requests failed in the responses
verbose_errors: bool = False,
# The operation mode in services.get's response
services_mode: eaapi.const.ServicesMode = eaapi.const.ServicesMode.Operation,
# The NTP server to use in services.get
ntp_server: str = "ntp://pool.ntp.org/",
# Keepalive server to use in serices.get. We'll use our own if one is not specified
keepalive_server: str = None
)
@handler(
# Module name to handle. Will curry if method is not provided
module,
# Method name to handle
method=None,
# The datecode prefix to match during routing
dc_prefix=None,
# The service to use. Likely `local` or `local2` when handling game functions
service=None
)
```

13
eaapi/server/__init__.py Normal file
View File

@ -0,0 +1,13 @@
from .server import EAMServer
from .context import CallContext
from .model import Model, ModelMatcher, DatecodeMatcher
from .exceptions import EAMHTTPException
from .controller import Controller
__all__ = (
"EAMServer",
"CallContext",
"Model", "ModelMatcher", "DatecodeMatcher",
"EAMHTTPException",
"Controller",
)

4
eaapi/server/__main__.py Normal file
View File

@ -0,0 +1,4 @@
from .server import EAMServer
app = EAMServer("http://127.0.0.1:5000", verbose_errors=True)
app.run("0.0.0.0", 5000, debug=True)

45
eaapi/server/const.py Normal file
View File

@ -0,0 +1,45 @@
# Services where the module and service name match
TRIVIAL_SERVICES = [
"pcbtracker",
"message",
"facility",
"pcbevent",
"cardmng",
"package",
"userdata",
"userid",
"dlstatus",
"eacoin",
# "traceroute",
"apsmanager",
"sidmgr",
]
# Just chilling here until I figure out where these route
UNMAPPED_SERVICES = {
"???0": "numbering",
"???1": "pkglist",
"???2": "posevent",
"???3": "lobby",
"???4": "lobby2",
"???5": "netlog", # ins.netlog (?)
"???6": "globby",
"???7": "matching",
"???8": "netsci",
}
MODULE_SERVICES = "services"
RESERVED_MODULES = [
MODULE_SERVICES,
]
METHOD_SERVICES_GET = "get"
SERVICE_SERVICES = "services"
SERVICE_KEEPALIVE = "keepalive"
SERVICE_NTP = "ntp"
RESERVED_SERVICES = [
SERVICE_SERVICES,
SERVICE_KEEPALIVE,
SERVICE_NTP,
]

105
eaapi/server/context.py Normal file
View File

@ -0,0 +1,105 @@
import eaapi
from . import exceptions as exc
from .model import Model
NODE_CALL = "call"
NODE_RESP = "response"
class CallContext:
def __init__(self, request, decoder, call, eainfo, compressed):
if call.name != NODE_CALL:
raise exc.CallNodeMissing
self._request = request
self._decoder = decoder
self._call: eaapi.XMLNode = call
self._eainfo: str | None = eainfo
self._compressed: bool = compressed
self._resp: eaapi.XMLNode = eaapi.XMLNode.void(NODE_RESP)
self._module: str | None = None
self._method: str | None = None
self._url_slash: bool | None = None
self._model: Model = Model.from_model_str(call.get("model"))
@property
def module(self):
return self._module
@property
def method(self):
return self._method
@property
def url_slash(self):
return self._url_slash
@property
def request(self):
return self._request
@property
def was_xml_string(self):
return self._decoder.is_xml_string
@property
def was_compressed(self):
return self._compressed
@property
def call(self):
return self._call
@property
def resp(self):
return self._resp
@property
def model(self):
return self._model
@property
def srcid(self):
return self._call.get("srcid")
@property
def tag(self):
return self._call.get("tag")
def get_root(self):
return self.call.xpath(self.module)
def abort(self, status="1"):
return self.resp.append(self.module, status=status)
def ok(self):
return self.abort("0")
class ResponseContext:
def __init__(self, resp, decoder, response, compressed):
if response.name != NODE_RESP:
raise exc.CallNodeMissing
self._resp = resp
self._decoder = decoder
self._response = response
self._compressed = compressed
@property
def resp(self):
return self._resp
@property
def decoder(self):
return self._decoder
@property
def response(self):
return self._response
@property
def compressed(self):
return self._compressed

210
eaapi/server/controller.py Normal file
View File

@ -0,0 +1,210 @@
from typing import Callable
from collections import defaultdict
from abc import ABC
from .context import CallContext
from .model import ModelMatcher
from .const import RESERVED_MODULES, RESERVED_SERVICES, TRIVIAL_SERVICES, MODULE_SERVICES, METHOD_SERVICES_GET
import eaapi
Handler = Callable[[CallContext], None]
class IController(ABC):
_name: str
def get_handler(self, ctx: CallContext) -> Handler | None:
raise NotImplementedError
def get_service_routes(self, ctx: CallContext | None) -> dict[str, str]:
raise NotImplementedError
def serviced_prefixes(self) -> list[str]:
raise NotImplementedError
class Controller(IController):
def __init__(self, server, endpoint="", matcher: None | ModelMatcher = None):
from .server import EAMServer
self._server: EAMServer = server
server.controllers.append(self)
import inspect
caller = inspect.getmodule(inspect.stack()[1][0])
assert caller is not None
self._name = caller.__name__
self._handlers: dict[
tuple[str | None, str | None],
list[tuple[ModelMatcher, Handler]]
] = defaultdict(lambda: [])
self._endpoint = endpoint
self._matcher = matcher
self._services: set[tuple[ModelMatcher, str]] = set()
self._pre_handler: list[Callable[[CallContext, Handler], Handler]] = []
@property
def server(self):
return self._server
def on_pre(self, callback):
if callback not in self._pre_handler:
self._pre_handler.append(callback)
def add_dummy_service(
self,
service: str,
matcher: ModelMatcher | None = None,
unsafe_force_bypass_reserved: bool = False
):
if not unsafe_force_bypass_reserved:
if service in RESERVED_SERVICES:
raise KeyError(
f"{service} is a reserved service provided by default.\n"
"Pass unsafe_force_bypass_reserved=True to override this implementation"
)
if matcher is None:
matcher = ModelMatcher()
self._services.add((matcher, service))
def register_handler(
self,
handler: Handler,
module: str,
method: str,
matcher: ModelMatcher | None = None,
service: str | None = None,
unsafe_force_bypass_reserved: bool = False
):
if not unsafe_force_bypass_reserved:
if module in RESERVED_MODULES:
raise KeyError(
f"{module} is a reserved module provided by default.\n"
"Pass unsafe_force_bypass_reserved=True to override this implementation"
)
if service is not None and service in RESERVED_SERVICES:
raise KeyError(
f"{service} is a reserved service provided by default.\n"
"Pass unsafe_force_bypass_reserved=True to override this implementation"
)
if service is None:
if module not in TRIVIAL_SERVICES:
raise ValueError(f"Unable to identify service for {module}")
service = module
handlers = self._handlers[(module, method)]
for i in handlers:
if matcher is None and i[0] is None:
raise ValueError(f"Duplicate default handler for {module}.{method}")
if matcher == i[0]:
raise ValueError(f"Duplicate handler for {module}.{method} ({matcher})")
matcher_ = matcher or ModelMatcher()
handlers.append((matcher_, handler))
handlers.sort(key=lambda x: x[0])
self._services.add((matcher_, service))
def handler(
self,
module: str,
method: str | None = None,
matcher: ModelMatcher | None = None,
service: str | None = None,
unsafe_force_bypass_reserved: bool = False
):
if method is None:
def h2(method):
return self.handler(module, method, matcher, service)
return h2
# Commented out for MachineCallContext bodge
# def decorator(handler: Handler):
def decorator(handler):
self.register_handler(handler, module, method, matcher, service, unsafe_force_bypass_reserved)
return handler
return decorator
def get_handler(self, ctx):
if self._matcher is not None:
if not self._matcher.matches(ctx.model):
return None
handlers = self._handlers[(ctx.module, ctx.method)]
if not handlers:
return None
for matcher, handler in handlers:
if matcher.matches(ctx.model):
for i in self._pre_handler:
handler = i(ctx, handler)
return handler
return None
def get_service_routes(self, ctx: CallContext | None):
endpoint = self._server.expand_url(self._endpoint)
if ctx is None:
return {
service: endpoint
for _, service in self._services
}
if self._matcher is not None and not self._matcher.matches(ctx.model):
return {}
return {
service: endpoint
for matcher, service in self._services
if matcher.matches(ctx.model)
}
def serviced_prefixes(self) -> list[str]:
return [self._endpoint]
class ServicesController(IController):
def __init__(self, server, services_mode: eaapi.const.ServicesMode):
from .server import EAMServer
self._server: EAMServer = server
self.services_mode = services_mode
self._name = __name__ + "." + self.__class__.__name__
def service_routes_for(self, ctx: CallContext):
services = defaultdict(lambda: self._server.public_url)
for service, route in self._server.get_service_routes(ctx):
services[service] = self._server.expand_url(route)
return services
def get_handler(self, ctx: CallContext) -> Handler | None:
if ctx.module == MODULE_SERVICES and ctx.method == METHOD_SERVICES_GET:
return self.services_get
return None
def service_route(self, for_service: str, ctx: CallContext):
routes = self.service_routes_for(ctx)
return routes[for_service]
def services_get(self, ctx: CallContext):
services = ctx.resp.append(
MODULE_SERVICES, expire="600", mode=self.services_mode.value, status="0"
)
routes = self._server.get_service_routes(ctx)
for service in routes:
services.append("item", name=service, url=routes[service])
def get_service_routes(self, ctx: CallContext | None) -> dict[str, str]:
return {}
def serviced_prefixes(self) -> list[str]:
return [""]

View File

@ -0,0 +1,51 @@
from werkzeug.exceptions import HTTPException
class EAMHTTPException(HTTPException):
code = None
eam_description = None
class InvalidUpstream(EAMHTTPException):
code = 400
eam_description = "Upstream URL invalid"
class UpstreamFailed(EAMHTTPException):
code = 400
eam_description = "Upstream request failed"
class UnknownCompression(EAMHTTPException):
code = 400
eam_description = "Unknown compression type"
class InvalidPacket(EAMHTTPException):
code = 400
eam_description = "Invalid XML packet"
class InvalidModel(EAMHTTPException):
code = 400
eam_description = "Invalid model"
class ModelMissmatch(EAMHTTPException):
code = 400
eam_description = "Model missmatched"
class ModuleMethodMissing(EAMHTTPException):
code = 400
eam_description = "Module or method missing"
class CallNodeMissing(EAMHTTPException):
code = 400
eam_description = "<call> node missing"
class NoMethodHandler(EAMHTTPException):
code = 404
eam_description = "No handler found for module/method"

201
eaapi/server/model.py Normal file
View File

@ -0,0 +1,201 @@
from dataclasses import dataclass
import eaapi
class Model:
def __init__(self, gamecode: str, dest: str, spec: str, rev: str, datecode: str):
self._gamecode = gamecode
self._dest = dest
self._spec = spec
self._rev = rev
self._datecode = datecode
@classmethod
def from_model_str(cls, model: str) -> "Model":
return cls(*eaapi.parse_model(model))
@property
def gamecode(self):
return self._gamecode
@property
def dest(self):
return self._dest
@property
def spec(self):
return self._spec
@property
def rev(self):
return self._rev
@property
def datecode(self):
return int(self._datecode)
@property
def year(self):
return int(self._datecode[:4])
@property
def month(self):
return int(self._datecode[4:6])
@property
def day(self):
return int(self._datecode[6:8])
@property
def minor(self):
return int(self._datecode[8:10])
def __hash__(self):
return hash(str(self))
def __eq__(self, other):
if not isinstance(other, Model):
return False
return str(other) == str(self)
def __str__(self):
return f"{self.gamecode}:{self.dest}:{self.spec}:{self.rev}:{self.datecode}"
def __repr__(self):
return f"<Model {self} at 0x{id(self):016X}>"
@dataclass
class DatecodeMatcher:
year: int | None = None
month: int | None = None
day: int | None = None
minor: int | None = None
@classmethod
def from_str(cls, datecode: str):
if len(datecode) != 10 or not datecode.isdigit():
raise ValueError("Not a valid datecode")
return cls(
int(datecode[0:4]),
int(datecode[4:6]),
int(datecode[6:8]),
int(datecode[8:10])
)
def _num_filters(self):
num = 0
if self.year is not None:
num += 1
if self.month is not None:
num += 1
if self.day is not None:
num += 1
if self.minor is not None:
num += 1
return num
def __lt__(self, other):
if self._num_filters() < other._num_filters():
return False
if self.minor is None and other.minor is not None:
return False
if self.day is None and other.day is not None:
return False
if self.month is None and other.month is not None:
return False
if self.year is None and other.year is not None:
return False
return True
def __hash__(self):
return hash(str(self))
def __str__(self):
year = self.year if self.year is not None else "----"
month = self.month if self.month is not None else "--"
day = self.day if self.day is not None else "--"
minor = self.minor if self.minor is not None else "--"
return f"{year:04}{month:02}{day:02}{minor:02}"
def matches(self, model):
if self.year is not None and model.year != self.year:
return False
if self.month is not None and model.month != self.month:
return False
if self.day is not None and model.day != self.day:
return False
if self.minor is not None and model.minor != self.minor:
return False
return True
@dataclass
class ModelMatcher:
gamecode: str | None = None
dest: str | None = None
spec: str | None = None
rev: str | None = None
datecode: list[DatecodeMatcher] | DatecodeMatcher | None = None
def _num_filters(self):
num = 0
if self.gamecode is not None:
num += 1
if self.dest is not None:
num += 1
if self.spec is not None:
num += 1
if self.rev is not None:
num += 1
if isinstance(self.datecode, list):
num += sum(i._num_filters() for i in self.datecode)
elif self.datecode is not None:
num += self.datecode._num_filters()
return num
def __lt__(self, other):
if self._num_filters() < other._num_filters():
return False
if self.datecode is None and other.datecode is not None:
return False
if self.rev is None and other.rev is not None:
return False
if self.spec is None and other.spec is not None:
return False
if self.dest is None and other.dest is not None:
return False
if self.gamecode is None and other.gamecode is not None:
return False
return True
def __hash__(self):
return hash(str(self))
def __str__(self):
gamecode = self.gamecode if self.gamecode is not None else "---"
dest = self.dest if self.dest is not None else "-"
spec = self.spec if self.spec is not None else "-"
rev = self.rev if self.rev is not None else "-"
datecode = self.datecode if self.datecode is not None else "-" * 10
if isinstance(self.datecode, list):
datecode = "/".join(str(i) for i in self.datecode)
if not datecode:
datecode = "-" * 10
return f"{gamecode:3}:{dest}:{spec}:{rev}:{datecode}"
def matches(self, model):
if self.gamecode is not None and model.gamecode != self.gamecode:
return False
if self.dest is not None and model.dest != self.dest:
return False
if self.spec is not None and model.spec != self.spec:
return False
if self.rev is not None and model.rev != self.rev:
return False
if isinstance(self.datecode, list):
return any(i.matches(model) for i in self.datecode)
if self.datecode is not None:
return self.datecode.matches(model)
return True

397
eaapi/server/server.py Normal file
View File

@ -0,0 +1,397 @@
import traceback
import urllib.parse
import urllib
import sys
import os
from typing import Callable
from collections import defaultdict
from werkzeug.exceptions import HTTPException, MethodNotAllowed
from werkzeug.wrappers import Request, Response
from werkzeug.routing import Map, Rule
import eaapi
from . import exceptions as exc
from .context import CallContext
from .model import Model
from .controller import IController, ServicesController
from .const import SERVICE_NTP, SERVICE_KEEPALIVE
Handler = Callable[[CallContext], None]
HEADER_ENCRYPTION = "X-Eamuse-Info"
HEADER_COMPRESSION = "X-Compress"
PINGABLE_IP = "127.0.0.1"
class NetworkState:
def __init__(self):
self._pa = PINGABLE_IP # TODO: what does this one mean?
self.router_ip = PINGABLE_IP
self.gateway_ip = PINGABLE_IP
self.center_ip = PINGABLE_IP
def format_ka(self, base):
return base + "?" + urllib.parse.urlencode({
"pa": self.pa,
"ia": self.ia,
"ga": self.ga,
"ma": self.ma,
"t1": self.t1,
"t2": self.t2,
})
@property
def pa(self) -> str:
return self._pa
@property
def ia(self) -> str:
return self.router_ip
@property
def ga(self) -> str:
return self.gateway_ip
@property
def ma(self) -> str:
return self.center_ip
# TODO: Identify what these values are. Ping intervals?
@property
def t1(self):
return 2
@property
def t2(self):
return 10
class EAMServer:
def __init__(
self,
public_url: str,
prioritise_params: bool = False,
verbose_errors: bool = False,
services_mode: eaapi.const.ServicesMode = eaapi.const.ServicesMode.Operation,
ntp_server: str = "ntp://pool.ntp.org/",
keepalive_server: str | None = None,
no_keepalive_route: bool = False,
disable_routes: bool = False,
no_services_handler: bool = False,
):
self.network = NetworkState()
self.verbose_errors = verbose_errors
self._prioritise_params = prioritise_params
self._public_url = public_url
self.disable_routes = disable_routes
self._no_keepalive_route = no_keepalive_route
self.ntp = ntp_server
self.keepalive = keepalive_server or f"{public_url}/keepalive"
self._prng = eaapi.crypt.new_prng()
self._setup = []
self._pre_handlers_check = []
self._teardown = []
self._einfo_ctx: CallContext | None = None
self._einfo_controller: str | None = None
self.controllers: list[IController] = []
if not no_services_handler:
self.controllers.append(ServicesController(self, services_mode))
def on_setup(self, callback):
if callback not in self._setup:
self._setup.append(callback)
def on_pre_handlers_check(self, callback):
if callback not in self._pre_handlers_check:
self._pre_handlers_check.append(callback)
def on_teardown(self, callback):
if callback not in self._teardown:
self._teardown.append(callback)
def build_rules_map(self) -> Map:
if self.disable_routes:
return Map([])
rules = Map([], strict_slashes=False, merge_slashes=False)
prefixes = {"/"}
for i in self.controllers:
for prefix in i.serviced_prefixes():
prefix = self.expand_url(prefix)
if not prefix.startswith(self._public_url):
continue
prefix = prefix[len(self._public_url):]
if prefix == "":
prefix = "/"
prefixes.add(prefix)
for i in prefixes:
rules.add(Rule(f"{i}/<model>/<module>/<method>", endpoint="xrpc_request"))
# WSGI flattens the // at the start
if i == "/":
rules.add(Rule("/<model>/<module>/<method>", endpoint="xrpc_request"))
rules.add(Rule(f"{i}", endpoint="xrpc_request"))
if not self._no_keepalive_route:
rules.add(Rule("/keepalive", endpoint="keepalive_request"))
return rules
def expand_url(self, url: str) -> str:
return urllib.parse.urljoin(self._public_url, url)
@property
def public_url(self) -> str:
return self._public_url
def get_service_routes(self, ctx: CallContext | None) -> dict[str, str]:
services: dict[str, str] = defaultdict(lambda: self.public_url)
services[SERVICE_NTP] = self.ntp
services[SERVICE_KEEPALIVE] = self.network.format_ka(self.keepalive)
for i in self.controllers:
services.update(i.get_service_routes(ctx))
return services
def _decode_request(self, request: Request) -> CallContext:
ea_info = request.headers.get(HEADER_ENCRYPTION)
compression = request.headers.get(HEADER_COMPRESSION)
compressed = False
if compression == eaapi.Compression.Lz77.value:
compressed = True
elif compression != eaapi.Compression.None_.value:
raise exc.UnknownCompression
payload = eaapi.unwrap(request.data, ea_info, compressed)
decoder = eaapi.Decoder(payload)
try:
call = decoder.unpack()
except eaapi.EAAPIException:
raise exc.InvalidPacket
return CallContext(request, decoder, call, ea_info, compressed)
def _encode_response(self, ctx: CallContext) -> Response:
if ctx._eainfo is None:
ea_info = None
else:
ea_info = eaapi.crypt.get_key(self._prng)
encoded = eaapi.Encoder.encode(ctx.resp, ctx.was_xml_string)
wrapped = eaapi.wrap(encoded, ea_info, ctx.was_compressed)
response = Response(wrapped, 200)
if ea_info:
response.headers[HEADER_ENCRYPTION] = ea_info
response.headers[HEADER_COMPRESSION] = (
eaapi.Compression.Lz77 if ctx.was_compressed
else eaapi.Compression.None_
).value
return response
def _create_ctx(
self,
url_slash: bool,
request: Request,
model: Model | None,
module: str,
method: str
) -> CallContext:
ctx = self._decode_request(request)
ctx._module = module
ctx._method = method
ctx._url_slash = url_slash
self._einfo_ctx = ctx
if ctx.model != model:
raise exc.ModelMissmatch
return ctx
def _handle_request(self, ctx: CallContext) -> Response:
for controller in self.controllers:
if (handler := controller.get_handler(ctx)) is not None:
self._einfo_controller = (
f"{controller._name}"
)
handler(ctx)
break
else:
raise exc.NoMethodHandler
return self._encode_response(ctx)
def on_xrpc_other(
self,
request: Request,
service: str | None = None,
model: str | None = None,
module: str | None = None,
method: str | None = None
):
if request.method != "GET" or not self.verbose_errors:
raise MethodNotAllowed
return Response(
f"XRPC running. model {model}, call {module}.{method} ({service})"
)
def keepalive_request(self) -> Response:
return Response(None)
def parse_request(
self,
request: Request,
service: str | None = None,
model: str | None = None,
module: str | None = None,
method: str | None = None
):
url_slash = bool(module and module and method)
model_param = request.args.get("model", None)
module_param = request.args.get("module", None)
method_param = request.args.get("method", None)
if "f" in request.args:
module_param, _, method_param = request.args.get("f", "").partition(".")
if self._prioritise_params:
model = model_param or model
module = module_param or module
method = method_param or method
else:
model = model or model_param
module = module or module_param
method = method or method_param
if module is None or method is None:
raise exc.ModuleMethodMissing
if model is None:
model_obj = None
else:
try:
model_obj = Model.from_model_str(model)
except eaapi.exception.InvalidModel:
raise exc.InvalidModel
return url_slash, service, model_obj, module, method
def on_xrpc_request(
self,
request: Request,
service: str | None = None,
model: str | None = None,
module: str | None = None,
method: str | None = None
):
url_slash, service, model_obj, module, method = self.parse_request(request, service, model, module, method)
if request.method != "POST":
return self.on_xrpc_other(request, service, model, module, method)
ctx = self._create_ctx(url_slash, request, model_obj, module, method)
for i in self._pre_handlers_check:
i(ctx)
return self._handle_request(ctx)
def _make_error(self, status: int | None = None, message: str | None = None) -> Response:
response = eaapi.XMLNode.void("response")
if status is not None:
response["status"] = str(status)
if self.verbose_errors:
if message:
response.append("details", eaapi.Type.Str, message)
context = response.append("context")
if self._einfo_ctx is not None:
context.append("module", eaapi.Type.Str, self._einfo_ctx.module)
context.append("method", eaapi.Type.Str, self._einfo_ctx.method)
context.append("game", eaapi.Type.Str, str(self._einfo_ctx.model))
if self._einfo_controller is not None:
context.append("controller", eaapi.Type.Str, self._einfo_controller)
encoded = eaapi.Encoder.encode(response, False)
wrapped = eaapi.wrap(encoded, None, False)
response = Response(wrapped, status or 500)
response.headers[HEADER_COMPRESSION] = eaapi.Compression.None_.value
return response
def _eamhttp_error(self, exc: exc.EAMHTTPException) -> Response:
return self._make_error(exc.code, exc.eam_description)
def _structure_error(self, e: eaapi.exception.XMLStrutureError) -> Response:
summary = traceback.extract_tb(e.__traceback__)
for frame_summary in summary:
filename = frame_summary.filename
frame_summary.filename = os.path.relpath(filename)
# The first three entries are within the controller, and the last one is us
summary = summary[3:-1]
tb = "".join(traceback.format_list(traceback.StackSummary.from_list(summary)))
tb += f"{e.__module__}.{e.__class__.__name__}"
return self._make_error(400, tb)
def _generic_error(self, exc: Exception) -> Response:
return self._make_error(500, str(exc))
def dispatch_request(self, request):
self._einfo_ctx = None
self._einfo_controller = None
adapter = self.build_rules_map().bind_to_environ(request.environ)
try:
endpoint, values = adapter.match()
return getattr(self, f"on_{endpoint}")(request, **values)
except exc.EAMHTTPException as e:
return self._eamhttp_error(e)
except HTTPException as e:
return e
except eaapi.exception.XMLStrutureError as e:
traceback.print_exc(file=sys.stderr)
return self._structure_error(e)
except Exception as e:
traceback.print_exc(file=sys.stderr)
return self._generic_error(e)
def wsgi_app(self, environ, start_response):
request = Request(environ)
response = self.dispatch_request(request)
return response(environ, start_response)
def __call__(self, environ, start_response):
for i in self._setup:
i()
try:
response = self.wsgi_app(environ, start_response)
for i in self._teardown:
i(None)
return response
except Exception as e:
for i in self._teardown:
i(e)
raise e
def run(self, host="127.0.0.1", port=5000, debug=False):
from werkzeug.serving import run_simple
run_simple(host, port, self, use_debugger=debug, use_reloader=debug)