PK!Waioli/__init__.py# -*- coding: utf-8 -*- from aioli.core.package import Package from .core import Application, YamlSettings from .core.manager import mgr from .exceptions import AioliException from .__version__ import __version__ PK! //aioli/__version__.py# -*- coding: utf-8 -*- __version__ = "0.2.1" PK!^ڦaioli/controller/__init__.py# -*- coding: utf-8 -*- from .base import BaseController from .injector import Injector from .decorators import route, input_load, output_dump PK!*|aioli/controller/base.py# -*- coding: utf-8 -*- from aioli.registry import RouteRegistry from aioli.core.component import BaseComponent class BaseController(BaseComponent): """Controller base class""" @classmethod def register(cls, pkg): """Makes the package available to this controller. :param pkg: aioli.Package """ cls._pkg_bind(pkg) async def on_ready(self): """Called upon initialization""" async def on_request(self, *args): """Called upon request arrival""" @property def stacks(self): """Route stack iterator Iterates over RouteStacks, takes the stack.name (which corresponds to the handler function name) and returns reference to its instance method, along with the stack itself (schemas, paths, methods etc). :return: (, ) """ for stack in RouteRegistry.stacks.values(): # Yield only if the stack belongs to the Controller being iterated on if stack.handler.__module__ == self.__module__: yield getattr(self, stack.name), stack def __repr__(self): return f'<{self.__class__.__name__} at {hex(id(self))}>' PK!wwaioli/controller/decorators.py# -*- coding: utf-8 -*- from enum import Enum from functools import wraps import ujson from aiohttp.web_request import Request from aiohttp.web_response import Response from aioli.registry import RouteRegistry class Method(Enum): GET = 'GET' POST = 'POST' PUT = 'PUT' PATCH = 'PATCH' DELETE = 'DELETE' HEAD = 'HEAD' CONNECT = 'CONNECT' OPTIONS = 'OPTIONS' TRACE = 'TRACE' def _schema_prepare(schema_cls, **kwargs): """Makes schema ready for use with Aioli 1) Takes a schema class 2) Creates `Meta` subclass if not set 3) Sets `ujson` render_module :param schema_cls: Marshmallow.Schema class :param kwargs: Kwargs to pass along to Marshmallow.Schema constructor :return: instance of `schema_cls` """ schema = schema_cls(**kwargs) from marshmallow.schema import SchemaMeta if not hasattr(schema, 'Meta'): schema.Meta = type('Meta', (SchemaMeta,), {}) m = schema.Meta m.render_module = ujson if not hasattr(m, 'load_only'): m.load_only = [] if not hasattr(m, 'dump_only'): m.dump_only = [] return schema def output_dump(schema_cls, status=200, many=False): """Returns a transformed and serialized Response :param schema_cls: Marshmallow.Schema class :param status: Return status (on success) :param many: Whether to return a list or single object :return: Response """ schema = _schema_prepare(schema_cls, many=many) def wrapper(fn): @wraps(fn) async def handler(*args, **kwargs): args_new = list(args) # Remove `Request` object from args (in case it wasn't consumed by `input_local`). if len(args) > 1 and isinstance(args[1], Request): args_new.pop(1) rv = await fn(*args_new, **kwargs) # Return HTTP encoded JSON response return Response( body=schema.dumps(rv, indent=4), status=status, content_type='application/json' ) stack = RouteRegistry.get_stack(handler) # Add the `response` schema to this route handler's stack stack.schemas.update({'response': schema}) return handler return wrapper def input_load(**schemas): """Takes a list of schemas used to validate and transform parts of a request object. The selected parts are injected into the route handler as arguments. :param schemas: list of schemas (kwargs) :return: Route handler """ schemas = {k: _schema_prepare(v) for k, v in schemas.items()} def wrapper(fn): @wraps(fn) async def handler(*args, **kwargs): args_new = list(args) request = kwargs['request'] if 'request' in kwargs else args_new.pop(1) if 'body' in schemas: kwargs.update({'body': schemas['body'].load(request.json)}) if 'query' in schemas: kwargs.update({'query': schemas['query'].load(request.query)}) return await fn(*args_new, **kwargs) stack = RouteRegistry.get_stack(handler) # Add the provided schemas to the RouteStack stack.schemas.update(schemas) return handler return wrapper def route(path, method, description=None, inject=None): """Prepares route registration, and performs handler injection. :param path: Handler path, relative to application and package paths :param method: HTTP Method :param inject: List of `Injector` injections :param description: Endpoint description :return: Route handler """ injections = inject or [] if isinstance(method, str): method = Method(method.upper()).value def wrapper(fn): @wraps(fn) async def handler(*inner_args, **inner_kwargs): request = inner_args[1] args_new = list(inner_args) # Handle injections and inject as kwargs. for injection in injections: if callable(injection.value): func = injection.value value = func(request) elif injection.name == 'request': # Full request injection, remove duplicate. value = args_new.pop(1) else: raise Exception('Unknown injection') inner_kwargs.update({injection.name: value}) return await fn(*args_new, **inner_kwargs) stack = RouteRegistry.get_stack(handler) # Adds the handler for registration once the loop is ready. stack.add_route(handler, path, method, description) return handler return wrapper PK!a aioli/controller/injector.py# -*- coding: utf-8 -*- from functools import partial from enum import Enum from aioli.utils import request_ip class Injector(Enum): remote_addr = partial(request_ip) request = 'request' PK!L9aioli/core/__init__.py# -*- coding: utf-8 -*- from .app import Application from .error import AioliErrorHandler from .settings import ApplicationSettings, YamlSettings from .manager import Manager PK!05 aioli/core/app.py# -*- coding: utf-8 -*- import logging import logging.config from aioli.log import LOGGING_CONFIG_DEFAULTS from .settings import ApplicationSettings from .error import AioliErrorHandler from .manager import mgr async def attach_manager(app): await mgr.attach(app, app.loop) class Application(web.Application): """Creates a Aioli application :param packages: List of (, ) :param path: Application root path :param settings: Settings overrides :param kwargs: kwargs to pass along to Sanic """ def __init__(self, packages=None, path='/api', cors_options=None, settings=None, **kwargs): if not packages: raise Exception(f'aioli.Application expects an iterable of packages, got: {type(packages)}') self.cors_options = cors_options or {} self.packages = packages try: overrides = dict(settings or {}) except ValueError: raise Exception('Application `settings` must be a collection') for name, logger in LOGGING_CONFIG_DEFAULTS['loggers'].items(): logger['level'] = 'DEBUG' if overrides.get('DEBUG') else 'INFO' super(Application, self).__init__( # log_config=kwargs.pop('log_config', LOGGING_CONFIG_DEFAULTS), **kwargs ) logging.config.dictConfig(LOGGING_CONFIG_DEFAULTS) # Application root logger self.log = logging.getLogger('root') # Apply known settings from ENV or provided `settings` self.config = ApplicationSettings(overrides, path).merged # Set up cross-origin resource sharing # CORS(self, supports_credentials=True, resources={r"/api/*": {"origins": "10.10.10.10"}}) self.error_handler = AioliErrorHandler() self.on_startup.append(attach_manager) # self.register_listener(mgr.attach, 'after_server_start') # self.register_listener(mgr.detach, 'after_server_stop') def run(self, host=None, port=None, workers=None, debug=False, sql_log=False, access_log=False, **kwargs): """Starts the HTTP server :param host: Listen host, defaults to 127.0.0.1 :param port: Listen port, defaults to 8080 :param workers: Number of workers, defaults to 1 per core. :param debug: Debugging :param sql_log: Log SQL-statements :param access_log: Log requests :param kwargs: Parameters to pass along to Sanic.run """ debug = debug or self.config['DEBUG'] workers = workers or self.config['WORKERS'] # Enable access and sql log by default in debug mode, otherwise disable if it wasn't explicitly enabled. if debug: self.log.warning('Debug mode enabled') sql_log = sql_log is None or sql_log access_log = access_log is None or access_log if int(workers) > 1: self.log.warning('Automatic reload DISABLED due to multiple workers') # Configure SQL statement logging # logging.getLogger('peewee').setLevel('DEBUG' if sql_log else 'INFO') access_log = logging.getLogger('aioli.request') cfg = dict( host=host or self.config['LISTEN_HOST'], port=port or self.config['LISTEN_PORT'], # workers=workers, # debug=debug, access_log_format='%a - %s %r', access_log=access_log, **kwargs ) # super(Application, self).run(**cfg) web.run_app(self, **cfg) PK!$gsaioli/core/component.py# -*- coding: utf-8 -*- class BaseComponent: pkg = None log = None path = None @classmethod def _pkg_bind(cls, pkg): cls.pkg = pkg cls.path = pkg.path cls.log = pkg.log PK!Fxaioli/core/error.py# -*- coding: utf-8 -*- from logging import getLogger from sanic.helpers import STATUS_CODES from sanic.handlers import ErrorHandler from sanic.exceptions import SanicException from marshmallow.exceptions import ValidationError from aioli.exceptions import AioliException from aioli.utils import jsonify log = getLogger('root') class AioliErrorHandler(ErrorHandler): """Error handler / dispatcher""" def __init__(self): super(AioliErrorHandler, self).__init__() def default(self, request, exception): if isinstance(exception, ValidationError): handler = validation elif isinstance(exception, AioliException): handler = aioli elif isinstance(exception, SanicException): handler = http_usage else: handler = fallback return handler(request, exception) def validation(_, exception): """Marshmallow validation error""" return jsonify({'error': exception.messages}, status=422) def aioli(_, exception): """Custom Aioli errors""" return jsonify({'error': str(exception)}, status=exception.status_code) def http_usage(_, exception): """Common HTTP errors that are not of AioliException type""" if hasattr(exception, 'args'): message = '\n'.join(exception.args) else: message = str(exception) # @TODO - Remove this block if https://github.com/huge-success/sanic/issues/1455 gets fixed. if 'HttpParserInvalidMethodError' in message: message = 'Invalid HTTP method' elif 'Traceback' in message: message = STATUS_CODES.get(exception.status_code) return jsonify({'error': message}, status=exception.status_code) def fallback(_, exception): """Unknown error - log it and return 500""" if hasattr(exception, 'status_code'): status = exception.status_code else: status = 500 log.exception(exception) message = STATUS_CODES.get(status) return jsonify({'error': message}, status=status) PK! BBaioli/core/manager.py# -*- coding: utf-8 -*- from os import getpid import inspect from functools import partial from aiohttp import ClientSession, web # from sanic_cors import CORS from aioli.core.package import Package from aioli.utils import format_path from aioli.db import Database class Manager: """Takes care of package registration and injection upon application start""" pkgs = [] app = None loop = None db = Database http_client: ClientSession @property def models(self): """Yields a list of package-model tuples""" for pkg_name, pkg in self.pkgs: for model in pkg.models: yield pkg, model def get_pkg(self, name): """Get package by name""" return dict(self.pkgs).get(name) def _load_packages(self, pkg_modules: list): """Takes a list of Packages, tests their sanity and registers with manager""" for assigned_path, module in pkg_modules: if not hasattr(module, 'export'): raise Exception(f'Missing package export in {module}') elif not isinstance(module.export, Package): raise Exception(f'Invalid package type {module.pkg}: must be of type {Package}') export = module.export if export.name in dict(self.pkgs).keys(): raise Exception(f'Duplicate package name {export.name}') export.version = getattr(module, '__version__', '0.0.0') export.path = assigned_path export.log.debug(f'Loading version {export.version}') self.pkgs.append((export.name, export)) async def _register_services(self): """Registers Services with Application""" for pkg_name, pkg in self.pkgs: for svc in pkg.services: # Make package available to Controller svc.register(pkg, self) svc_obj = svc() if inspect.iscoroutinefunction(svc_obj.on_ready): await svc_obj.on_ready() else: svc_obj.on_ready() pkg.log.info(f'Service {svc.__name__} initialized') def _register_models(self): """Registers Models with the application, and creates non-existent tables""" models = list(self.models) registered = [] if models and not self.db.connection: raise Exception('Unable to register models without a database connection') for pkg, model in models: if not hasattr(model, 'Meta'): meta = type('ModelMeta', (object, ), {}) model.Meta = meta # Inject model meta and set its namespace meta = model._meta meta.database = self.db.connection meta.table_name = f'{pkg.name}__{meta.table_name}' pkg.log.debug(f'Registering model: {model.__name__} [{meta.table_name}]') registered.append(model) with self.db.connection.allow_sync(): self.db.connection.create_tables(registered, safe=True) async def _register_controllers(self): """Registers Controllers with Application""" for pkg_name, pkg in self.pkgs: pkg.log.info('Controller initializing') path_base = self.app.config['API_BASE'] # Make package available to Controller pkg.controller.register(pkg) ctrl = pkg.controller = pkg.controller() # Iterate over route stacks and register routes with the application for handler, route in ctrl.stacks: handler_addr = hex(id(handler)) handler_name = f'{pkg_name}.{route.name}' path_full = format_path(path_base, pkg.path, route.path) pkg.log.debug( f'Registering route: {path_full} [{route.method}] => ' f'{route.name} [{handler_addr}]' ) # Register with Sanic method = str(route.method).lower() get_route = partial(getattr(web, method)) self.app.add_routes([get_route(path_full, handler)]) # Inject full path to route handler route.path_full = path_full # Let the Controller know we're ready to receive requests if inspect.iscoroutinefunction(ctrl.on_ready): await ctrl.on_ready() else: ctrl.on_ready() pkg.log.info('Controller initialized') def register_cors(self): cors_options = self.app.cors_options or {} # CORS(self.sanic, **cors_options) async def detach(self, *_): """Application stop-handler""" await self.http_client.close() async def attach(self, app, loop): """Application start-handler Sets up DB and HTTP clients, followed by component registration. :param app: Instance of aioli.Application :param loop: Instance of `uvloop.Loop` """ self._load_packages(app.packages) self.app = app self.loop = loop self.http_client = ClientSession(loop=loop) if app.config['DB_URL']: self.db.register(app, loop) self._register_models() await self._register_controllers() await self._register_services() app.log.info(f'Worker {getpid()} ready for action') mgr = Manager() PK!/Qaioli/core/package.py# -*- coding: utf-8 -*- import re from logging import getLogger from aioli.utils import validated_identifier class Package: """Associates components and meta with a package, for registration with a Aioli Application. :param name: Package name :param description: Package description :param controller: Controller class :param services: List of Service objects :param models: List of Model classes """ _path = None def __init__(self, name, description, controller=None, services=None, models=None): self.controller = controller self.services = services or [] self.models = models or [] self.name = validated_identifier(name) self.description = description self.log = getLogger(f'aioli.pkg.{self.name}') @property def path(self): """Package path accessor""" return self._path @path.setter def path(self, value): if not re.match(r'^/[a-zA-Z0-9-]*$', value): raise Exception(f'Package {self.name} path must be a valid path, example: /my-package-1') self._path = value def __repr__(self): return f'<{self.__class__.__name__} [{self.name}] at {hex(id(self))}>' PK!'{aioli/core/settings.py# -*- coding: utf-8 -*- from os import environ as env from multiprocessing import cpu_count from marshmallow import Schema, fields, post_load from aioli.utils import yaml_parse class YamlSettings: content = {} def __init__(self, files): for file in files: self.content.update(dict(yaml_parse(file, keys_to_upper=True)).items()) def __iter__(self): for k, v in self.content.items(): yield k, v class CoreSchema(Schema): listen_host = fields.String(missing='127.0.0.1') listen_port = fields.Integer(missing=5000) db_url = fields.String(missing=None) api_base = fields.String(missing='/api') debug = fields.Bool(missing=False) workers = fields.Integer(missing=cpu_count()) logo = fields.String(allow_none=True, missing=None) request_max_size = fields.Integer(missing=1000000) request_timeout = fields.Integer(missing=60) @post_load def jet_fmt(self, data): """Converts keys to uppercase, to conform with Sanic standards. :param data: settings :return: settings with uppercase keys """ transformed = {} for k, v in data.items(): transformed[k.upper()] = v return transformed class ApplicationSettings: def __init__(self, overrides, path=None): self._overrides = overrides self._overrides['api_base'] = path @property def merged(self): schema = CoreSchema() settings = {} for name, field in CoreSchema._declared_fields.items(): nu = name.upper() if nu in env: # Prefer environ value = env.get(nu) if isinstance(field, fields.Integer): value = int(value) if isinstance(field, fields.Boolean): value = str(value).strip().lower() in ['1', 'true', 'yes'] settings[name] = value elif nu in self._overrides: settings[name] = self._overrides[nu] return schema.load(settings) PK!Bꫤ aioli/db.py# -*- coding: utf-8 -*- from urllib.parse import urlparse, parse_qs from peewee_async import Manager, PooledMySQLDatabase, PooledPostgresqlDatabase class Database: connection: PooledPostgresqlDatabase manager: Manager @classmethod def register(cls, app, loop): db_url = app.config.get('DB_URL') if not db_url: return cls.connection = cls._create_connection(db_url) cls.connection.allow_sync = False cls.manager = Manager(cls.connection, loop=loop) @classmethod def _create_connection(cls, url): parsed = urlparse(url) if parsed.scheme == 'postgres': interface = PooledPostgresqlDatabase elif parsed.scheme == 'mysql': interface = PooledMySQLDatabase else: raise Exception(f'Database URL scheme must be "mysql" or "postgres", ' f'example: postgres://user:pass@127.0.0.1/database') params = parse_qs(parsed.query) pool_min = params.pop('pool_min', [5])[0] pool_max = params.pop('pool_max', [50])[0] settings = dict( database=parsed.path.lstrip('/'), user=parsed.username, password=parsed.password, host=parsed.hostname, port=parsed.port, min_connections=int(pool_min), max_connections=int(pool_max), ) return interface(**settings) PK!s(Caioli/exceptions.py# -*- coding: utf-8 -*- from sanic.exceptions import SanicException class AioliException(SanicException): def __init__(self, *args, **kwargs): self.log_message = kwargs.pop('write_log', False) super(AioliException, self).__init__(*args, **kwargs) PK!4ll aioli/log.py# -*- coding: utf-8 -*- import sys LOGGING_CONFIG_DEFAULTS = dict( version=1, disable_existing_loggers=False, loggers={ 'root': { 'level': 'INFO', 'handlers': ['console'] }, 'peewee': { 'level': 'INFO', 'handlers': ['console'], 'propagate': True, }, 'aioli.pkg': { 'level': 'INFO', 'handlers': ['pkg_console'], 'propagate': False, }, 'aioli.request': { 'level': 'INFO', 'handlers': ['request_console'], 'propagate': False, } }, handlers={ 'console': { 'class': 'logging.StreamHandler', 'formatter': 'generic', 'stream': sys.stdout }, 'request_console': { 'class': 'logging.StreamHandler', 'formatter': 'access', 'stream': sys.stdout, }, 'access_console': { 'class': 'logging.StreamHandler', 'formatter': 'access', 'stream': sys.stdout, }, 'pkg_console': { 'class': 'logging.StreamHandler', 'formatter': 'pkg', 'stream': sys.stdout }, }, formatters={ 'pkg': { 'format': '[%(levelname)1.1s %(asctime)s.%(msecs)03d %(name)s] %(message)s', 'datefmt': '%Y-%m-%d %H:%M:%S', 'class': 'logging.Formatter' }, 'access': { 'format': '[%(levelname)1.1s %(asctime)s.%(msecs)03d %(process)d] %(message)s', 'datefmt': '%Y-%m-%d %H:%M:%S', 'class': 'logging.Formatter', }, 'generic': { 'format': '[%(levelname)1.1s %(asctime)s.%(msecs)03d] %(message)s', 'datefmt': '%Y-%m-%d %H:%M:%S', 'class': 'logging.Formatter' }, } ) PK!aioli/registry.py# -*- coding: utf-8 -*- class RouteStack: """Keeps track of decorators applied to a route handler""" handler = None name = None path = None method = None description = None def __init__(self): self.schemas = { 'response': None, 'query': None, 'body': None } def __dict__(self): return self.__class__.__dict__ def add_route(self, handler, path, method, description): """Adds new route to the stack :param handler: Route handler function :param path: Route path :param method: Route method :param description: Endpoint description """ self.handler = handler self.name = handler.__name__ self.path = path self.method = method self.description = description class RouteRegistry: """Keeps track of all RouteStacks in the application""" stacks = {} @classmethod def get_stack(cls, handler): """Get (or create) new RouteStack for a handler :param handler: route handler :return: RouteStack instance """ hid = hash(handler.__module__ + handler.__name__) if hid not in cls.stacks: cls.stacks[hid] = RouteStack() return cls.stacks[hid] PK!paioli/schema.py# -*- coding: utf-8 -*- from marshmallow import * from marshmallow import validate class ParamsSchema(Schema): _limit = fields.Integer(missing=100, validate=validate.Range(min=0)) _offset = fields.Integer(missing=0, validate=validate.Range(min=0)) _sort = fields.String(missing='') class CountSchema(Schema): count = fields.Integer() class DeleteSchema(Schema): deleted = fields.Integer() PK!> {{aioli/service/__init__.py# -*- coding: utf-8 -*- from .base import BaseService from .db import DatabaseService from .http import HttpClientService PK!,E99aioli/service/base.py# -*- coding: utf-8 -*- from uvloop.loop import Loop from aioli import Application from aioli.core.component import BaseComponent class BaseService(BaseComponent): """The Service base class used for creating Package Services, it implements the singleton pattern as Services are commonly used in many parts of a Package. :ivar app: Aioli application instance :ivar loop: Asyncio event loop (uvloop) """ __instance = None def __new__(cls, *args, **kwargs): if not cls.__instance: cls.__instance = super(BaseService, cls).__new__(cls, *args, **kwargs) return cls.__instance app: Application loop: Loop def on_ready(self): """Called upon initialization""" @classmethod def register(cls, pkg, mgr): """Class method used internally by the Aioli manager to register a Service :param pkg: instance of :class:`aioli.Package` :param mgr: instance of :class:`Aioli.AioliManager` """ cls.loop = mgr.loop cls.app = mgr.app cls._pkg_bind(pkg) PK!;;aioli/service/db.py# -*- coding: utf-8 -*- import peewee from peewee_async import Manager from aioli.exceptions import AioliException from .base import BaseService class DatabaseService(BaseService): """Service class providing an interface with common database operations :ivar __model__: Peewee.Model :ivar db_manager: Aioli database manager (peewee-async) """ __model__: peewee.Model db_manager: Manager @classmethod def register(cls, pkg, mgr): super(DatabaseService, cls).register(pkg, mgr) cls.db_manager = mgr.db.manager @property def model(self): """Return service model or raise an exception :return: peewee.Model """ if not self.__model__: raise Exception(f'{self.__class__.__name__}.__model__ not set, unable to perform database operation') return self.__model__ def _select(self, *fields): """Creates a new select query :param fields: fields tuple :return: peewee.ModelSelect """ return self.model.extended(*fields) if hasattr(self.model, 'extended') else self.model.select(*fields) def _model_has_attrs(self, *attrs): """Ensure model has field(s) :param attrs: fields tuple :return: True or Exception """ for attr in attrs: if not hasattr(self.model, attr): raise Exception(f'Unknown field {attr}', 400) return True def _parse_sortstr(self, value: str): """Takes a comma-separated string of fields to sort in asc or desc order example: "-field1,field2" to order by field1 desc, field2 asc :param value: sortstr :return: list of sort-objects """ if not value: return [] model = self.model for col_name in value.split(','): sort_asc = True if col_name.startswith('-'): col_name = col_name[1:] sort_asc = False if self._model_has_attrs(col_name): sort_obj = getattr(model, col_name) yield sort_obj.asc() if sort_asc else sort_obj.desc() def _get_query_filtered(self, expression: peewee.Expression = None, select: peewee.ModelSelect = None, **params): """Constructs a query using an Expression and/or parameters. :param expression: peewee.Expression :param select: peewee.ModelSelect :param params: Dictionary for controlling pagination and sorting :return: Peewee query """ query_base = select or self._select() sort = params.pop('_sort', None) limit = params.pop('_limit', 0) offset = params.pop('_offset', 0) query = query_base.limit(limit).offset(offset) if isinstance(expression, peewee.Expression): query = query.where(expression) elif params: query = query.filter(**params) if sort: order = self._parse_sortstr(params['sort']) return query.order_by(*order) return query async def get_many(self, expression=None, **kwargs): """Returns a list of zero or more records :param expression: peewee.Expression (optional) :param kwargs: kwargs to pass along to `_get_query_filtered` :return: [peewee.Model] """ query = self._get_query_filtered(expression, **kwargs) return [o for o in await self.db_manager.execute(query)] async def get_by_pk(self, record_id: int): """Returns the matching record or raises an exception :param record_id: the record to query for :return: peewee.Model """ return await self.get_one(self.__model__.id == record_id) async def create(self, item: dict): """Creates new record, returning the record upon success :param item: item to be inserted :return: peewee.Model """ return await self.db_manager.create(self.model, **item) async def get_or_create(self, item: dict): """Get an object or create it with the provided defaults :param item: item to be fetched or inserted :return: peewee.Model """ return await self.db_manager.get_or_create(self.model, **item) async def count(self, expression=None, **kwargs): """Return the number of records the given query would yield :param expression: peewee.Expression (optional) :param kwargs: kwargs to pass along to `_get_query_filtered` :return: count (int) """ query = self._get_query_filtered(expression, **kwargs) return await self.db_manager.count(query) async def get_one(self, *args, **kwargs): """Get exactly one record or raise an exception :param args: peewee.Expression (optional) :param kwargs: kwargs to pass along to `_get_query_filtered` :return: peewee.Model """ query = self._get_query_filtered(*args, **kwargs) try: return await self.db_manager.get(query) except peewee.DoesNotExist: raise AioliException('No such record', 404) async def update(self, record, payload): """Updates a record in the database :param record: peewee.Model or PK :param payload: update payload :return: updated record """ if not isinstance(record, peewee.Model): record = await self.get_by_pk(record) for k, v in payload.items(): setattr(record, k, v) await self.db_manager.update(record) return record async def delete(self, record_id: int): """Deletes a record by id :param record_id: record id :return: {'deleted': True|False} """ record = await self.get_by_pk(record_id) deleted = await self.db_manager.delete(record) return {'deleted': deleted} PK!ډIaioli/service/http.py# -*- coding: utf-8 -*- from aiohttp import ClientSession from .base import BaseService class HttpClientService(BaseService): """Service class providing functions for interacting with HTTP servers :ivar http_client: aiohttp.ClientSession instance """ http_client: ClientSession async def http_request(self, *args, **kwargs): """Creates a custom HTTP request :param args: args to pass along to `aiohttp.ClientSession.request` :param kwargs: kwargs to pass along to `aiohttp.ClientSession.request` :return: aiohttp.ClientResponse """ async with self.http_client.request(*args, **kwargs) as resp: result = await resp.json() return result, resp.status async def http_post(self, url: str, payload: dict): """Creates a new HTTP POST request :param url: URL to send the request to :param payload: Payload to send :return: aiohttp.ClientResponse """ return await self.http_request('POST', url, data=payload) async def http_get(self, url: str): """Creates a new HTTP GET request :param url: URL to send the request to :return: aiohttp.ClientResponse """ return await self.http_request('GET', url) @classmethod def register(cls, pkg, mgr): super(HttpClientService, cls).register(pkg, mgr) cls.http_client = mgr.http_client PK!/  aioli/utils.py# -*- coding: utf-8 -*- import os import re import yaml import ujson from aiohttp.web import Response def format_path(*parts): path = '' for part in parts: path = f'/{path}/{part}' return re.sub(r'/+', '/', path.rstrip('/')) def validated_identifier(value): if not re.match(r'^[a-zA-Z0-9-]*$', value): raise Exception( f'Invalid identifier "{value}" - may only contain alphanumeric and hyphen characters.' ) return value def jsonify(data, **kwargs): response = ujson.dumps(data, indent=4) return Response(body=response, **kwargs) def request_ip(request): return request.remote_addr or request.ip def yaml_parse(file, keys_to_upper=False): settings_dir = os.environ.get('JET_SETTINGS_DIR', 'settings') path = f'{settings_dir}/{file}' with open(path, 'r') as stream: for key, value in yaml.load(stream).items(): if keys_to_upper: yield key.upper(), value else: yield key, value PK!(88aioli-0.2.2.dist-info/LICENSEBSD 2-Clause License Copyright (c) 2019, Robert Wikman All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. PK!HڽTUaioli-0.2.2.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!HBAWaioli-0.2.2.dist-info/METADATAO0Wܣ&6Em#Q(Qiv! />ޘ ji (Hd(WF1V5,qE3A<4LRSsg%YX˾/h| A:Â+ LTƴ_KZ! :o6Jy0a_kfq1jvspeKμn"eՆ-[2*FejTsy>`q%BE-[;Bm&#Ͽ˪R4dPGT f-kG1YR8ݧ<ՉO!TWZPK!H:4aioli-0.2.2.dist-info/RECORDuɒZy} V77@zD:TDAVe9Qޔ0ß" 3ra+~)i:PΗ#FvySs 2ɁHED`jw.0ѯ˿s%׌Ng" !Kɶ[4ND]^8:i%an3,DXϾ#bwZ]߾gcJqKMJƈ#[r UGT*1ǁw{ `[E„sNZnBnz-a5'(nCg7;Ўy)46j]ƅH.q$C I_[q}E.s.1iEb=4vFоtӀz%WXDA瀱 8t,Dh"n}k# K, JC'(7lr[bF# e]_gWYhجE|/dDac;'~Z]^/M(U-#KO:9w>b%<%$O#$%^dA>`>/ ckYQF@eWb':MCY}5O(弃r'n1.z5TM "ttx?PK!Waioli/__init__.pyPK! //aioli/__version__.pyPK!^ڦgaioli/controller/__init__.pyPK!*|1aioli/controller/base.pyPK!wwaioli/controller/decorators.pyPK!a aioli/controller/injector.pyPK!L9aioli/core/__init__.pyPK!05 aioli/core/app.pyPK!$gs)aioli/core/component.pyPK!Fx*aioli/core/error.pyPK! BB2aioli/core/manager.pyPK!/Q?Haioli/core/package.pyPK!'{9Maioli/core/settings.pyPK!Bꫤ |Uaioli/db.pyPK!s(CI[aioli/exceptions.pyPK!4ll \aioli/log.pyPK!daioli/registry.pyPK!pliaioli/schema.pyPK!> {{9kaioli/service/__init__.pyPK!,E99kaioli/service/base.pyPK!;;Wpaioli/service/db.pyPK!ډIÇaioli/service/http.pyPK!/  aioli/utils.pyPK!(88ϑaioli-0.2.2.dist-info/LICENSEPK!HڽTUBaioli-0.2.2.dist-info/WHEELPK!HBAWϗaioli-0.2.2.dist-info/METADATAPK!H:4baioli-0.2.2.dist-info/RECORDPK`