From cf75aec2a667fbc3421aa9a69f09dab2511c75cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Abdelkader=20Mart=C3=ADnez=20P=C3=A9rez?= Date: Tue, 2 Apr 2019 12:52:07 +0200 Subject: [PATCH] Documentation. --- kapow.py | 60 +++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/kapow.py b/kapow.py index bc0fe14..c72d46d 100644 --- a/kapow.py +++ b/kapow.py @@ -1,5 +1,8 @@ #!/usr/bin/env python +""" +A Kapow! interpreter written in Python. +""" from aiohttp import web from collections import defaultdict from dataclasses import dataclass @@ -20,10 +23,6 @@ import click # Parser # ######################################################################## -# -# Endpoint Definition -# - # Method method = ( Literal('GET') | Literal('POST') @@ -43,12 +42,13 @@ urlpattern = Combine(OneOrMore(p_pattern | p_path))(name="urlpattern") # Body body = (Suppress('{') + SkipTo(Combine(LineStart() + '}' + LineEnd()))(name="body")) +# Endpoint head endpoint = (Optional(method_spec + Suppress(White()), default='*')(name="method") + urlpattern + Suppress(White())) -# Endpoint +# Endpoint bodies code_ep = (endpoint + body)(name="code_ep") path_ep = (endpoint + '=' + SkipTo(LineEnd())(name="path"))(name="path_ep") @@ -66,7 +66,9 @@ class ResourceManager: async def get_value(context, path): + """Return the value of an http resource.""" def nrd(n): + """Return the nrd element in a path.""" return path.split('/', n)[-1] if path == 'request/method': @@ -88,6 +90,13 @@ async def get_value(context, path): async def set_value(context, path, value): + """ + Write to an http resource. + + File-like resources like `body` get write() calls so they have + append semantics. Non file-like resources are just set. + + """ def nrd(n): return path.split('/', n)[-1] @@ -112,10 +121,28 @@ def is_writable(path): def get_manager(resource, context): + """ + Return an async context manager capable of manage the given + resource. + """ view, path = resource.split(':') @contextlib.asynccontextmanager async def manager(): + """ + Manage the given `resource` as an async context manager. + + This context manager has different behavior depending on the + `view` and/or `path` of the resource. + + As a context manager it has three sections: + - Before `yield`: Prepare, if needed, the physical resource on + disk. + - `yield`: Return a `ResourceManager` object containing the + shell representation of the object and the coroutine + consuming/generating the resource data. + - After `yield`: Cleanup any disk resource. + """ if view == 'raw': if not is_readable(path): raise ValueError(f'Non-readable path "{path}".') @@ -137,7 +164,6 @@ def get_manager(resource, context): # https://stackoverflow.com/a/1430566 filename = tempfile.mktemp() os.mkfifo(filename) - if path.startswith('response/stream'): async def manage_fifo(): initialized = False @@ -179,11 +205,9 @@ def get_manager(resource, context): raise RuntimeError('WTF!') finally: os.unlink(filename) - yield ResourceManager( shell_repr=shell_quote(filename), coro=manage_fifo()) - elif view == 'file': with tempfile.NamedTemporaryFile(mode='w+b', buffering=0) as tmp: if is_readable(path): @@ -198,10 +222,6 @@ def get_manager(resource, context): if is_writable(path): tmp.seek(0) await set_value(context, path, tmp.read()) - elif view == 'source': - raise NotImplementedError('source view not implemented') - elif view == 'sink': - raise NotImplementedError('sink view not implemented') else: raise ValueError(f'Unknown view type {view}') @@ -209,10 +229,13 @@ def get_manager(resource, context): class KapowTemplate(Template): + """Shell-code templating for @view:path variables substitution""" + delimiter = '@' idpattern = r'(?a:[_a-z][_a-z0-9]*:[_a-z][-_a-z0-9/]*)' async def run(self, context): + """Run this template allocating and deallocating resources.""" async with contextlib.AsyncExitStack() as stack: # Initialize all resources creating a mapping resources = dict() # resource: (shell_repr, manager) @@ -243,11 +266,10 @@ class KapowTemplate(Template): task.cancel() await asyncio.sleep(0) -def create_runner(code): - return KapowTemplate(code).run def create_context(request): + """Create a request context with default values.""" context = dict() context["request"] = request context["stream"] = None @@ -258,6 +280,7 @@ def create_context(request): async def response_from_context(context): + """Return the appropia aiohttp response for a given context.""" if context["stream"] is not None: await context["stream"].write_eof() return context["stream"] @@ -269,15 +292,16 @@ async def response_from_context(context): def generate_endpoint(code): + """Return an aiohttp-endpoint coroutine to run kapow `code`.""" async def endpoint(request): context = create_context(request) - runner = create_runner(code) - await runner(context) # Will change context + await KapowTemplate(code).run(context) # Will change context return (await response_from_context(context)) return endpoint def path_server(path): + """Return an aiohttp-endpoint coroutine to serve the file in `path`.""" # At initialization check if not os.path.isfile(path): raise NotImplementedError("Only files can be served.") @@ -289,17 +313,20 @@ def path_server(path): return web.FileResponse(path) return serve_path + ######################################################################## # Webserver # ######################################################################## def register_code_endpoint(app, methods, pattern, code): + """Register all needed endpoints for the defined endpoint code.""" print(f"Registering [code] methods={methods!r} pattern={pattern!r}") endpoint = generate_endpoint(code) for method in methods: # May be '*' app.add_routes([web.route(method, pattern, endpoint)]) def register_path_endpoint(app, methods, pattern, path): + """Register all needed endpoints for the defined file.""" print(f"Registering [path] methods={methods!r} pattern={pattern!r}") for method in methods: # May be '*' app.add_routes([web.route(method, pattern, path_server(path))]) @@ -310,6 +337,7 @@ def register_path_endpoint(app, methods, pattern, path): @click.argument('program', type=click.File(), required=False) @click.pass_context def main(ctx, program, expression): + """Run the kapow server with the given command-line parameters.""" if program is None and expression is None: click.echo(ctx.get_help()) ctx.exit()