# -*- coding: utf-8 -*-
from setuptools import setup

packages = \
['vial']

package_data = \
{'': ['*']}

setup_kwargs = {
    'name': 'pyvial',
    'version': '1.2.0',
    'description': 'A micro web framework for AWS Lambda.',
    'long_description': '# Vial\n<p>\n    <a href="https://pypi.org/project/pyvial/">\n        <img src="https://badgen.net/pypi/v/pyvial" alt="Latest Version" style="max-width:100%;">\n    </a>\n    <a href="https://github.com/michaeldimchuk/pyvial/actions/workflows/tests.yaml">\n        <img src="https://github.com/michaeldimchuk/pyvial/actions/workflows/tests.yaml/badge.svg" alt="Test Status" style="max-width:100%;">\n    </a>\n    <a href="https://github.com/pre-commit/pre-commit">\n        <img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white" alt="Pre-Commit Enabled" style="max-width:100%;">\n    </a>\n</p>\n\nVial is an unopinionated micro web framework for AWS Lambda. It\'s main purpose is to provide an easy to use\ninterface while also making the core functionality of the framework as modular as possible.\n\n## Installation\nTo add vial to your project, run the following command:\n```\npoetry add pyvial\n```\n\n## Usage\n### Entry Point\nThe main entry point of the application is always the `Vial#__call__` function. When deploying to AWS Lambda,\nthe Lambda handler should point to the `Vial` object in whichever file it\'s defined in. As an example:\n```\nfrom vial.app import Vial\n\napp = Vial(__name__)\n```\nIf this code snippet is defined in an `app.py` file, the handler would be `app.app`.\n\n### Basic API\n```\nfrom vial.app import Vial\n\napp = Vial(__name__)\n\n\n@app.get("/health")\ndef health() -> dict[str, str]:\n    return {"status": "OK"}\n```\nA test case with this example is available in [tests/samples/test_with_app.py](tests/samples/test_with_app.py).\n\nThe project can be deployed with the [serverless framework](https://www.serverless.com/framework/docs/getting-started).\nTo get started with serverless for the first time, run the following commands in your project directory:\n```\nyarn add --dev serverless serverless-python-requirements\n```\n\nBelow is a basic `serverless.yml` file to configure and deploy the project. It assumes that the only file to deploy\nis app.py, and the Vial object is defined there as a variable named `app`.\n```\nservice: store\n\ncustom:\n  pythonRequirements:\n    usePoetry: true\n    slim: true\n\nprovider:\n  name: aws\n  runtime: python3.9\n  memorySize: 128\n  region: us-west-2\n\nfunctions:\n  api:\n    handler: app.app\n    events:\n      - http: get /health\n\nplugins:\n  - serverless-python-requirements\n\npackage:\n  patterns:\n    - "app.py"\n    - "!.*/**"\n    - "!package.json"\n    - "!yarn.json"\n    - "!node_modules"\n    - "poetry.lock"\n    - "pyproject.toml"\n```\nYou can now deploy the project with `yarn run serverless deploy`.\n\nAn example of this application is available at [samples/store/serverless.yml](samples/store/serverless.yml).\n\n### Current Request\nThe current request is tracked within a contextual object that wraps the lambda request, and can be accessed\nthrough the `vial.request` module. The `request.get()` function is only available during a lambda request,\nand will raise an error if called outside of one. It can be accessed like so:\n```\nfrom vial import request\nfrom vial.app import Vial\nfrom vial.types import Request\n\napp = Vial(__name__)\n\n\n@app.get("/hello-world")\ndef hello_world() -> dict[str, list[str]]:\n    if not (query_params := request.get().query_parameters):\n        raise ValueError("Must provide at least one query parameter")\n    return dict(query_params)\n```\nA test case with this example is available in [tests/samples/test_with_current_request.py](tests/samples/test_with_current_request.py).\n\n### Path Parameters\nYou can define path parameters like this:\n```\nfrom dataclasses import dataclass\n\nfrom vial.app import Vial\n\napp = Vial(__name__)\n\n\n@dataclass\nclass User:\n    user_id: str\n\n\n@app.get("/users/{user_id}")\ndef get_user(user_id: str) -> User:\n    return User(user_id)\n```\nA test case with this example is available in [tests/samples/test_with_path_parameters.py](tests/samples/test_with_path_parameters.py).\n\nVial supports some path parameter parsing as part of the invocation process. For example when using a UUID\nas a path parameter, Vial can convert it from a string to a UUID automatically:\n```\nfrom dataclasses import dataclass\nfrom uuid import UUID, uuid4\n\nfrom vial.app import Vial\n\napp = Vial(__name__)\n\n\n@dataclass\nclass User:\n    user_id: UUID\n\n\n@app.get("/users/{user_id:uuid}")\ndef get_user(user_id: UUID) -> User:\n    if not isinstance(user_id, UUID):\n        raise AssertionError("Invalid input")\n    return User(user_id)\n```\nA test case with this example is available in [tests/samples/test_with_parser.py](tests/samples/test_with_parser.py).\n\nThe following parsers are supported by default:\n\n| Parser        | Type              |\n| ------------- | ----------------- |\n| `str`         | `str`             |\n| `bool`        | `bool`            |\n| `int`         | `int`             |\n| `float`       | `float`           |\n| `decimal`     | `decimal.Decimal` |\n| `uuid`        | `uuid.UUID`       |\n\nYou can register your own parser that consumes a string variable and converts it to any other type.\nAs parsers are bound directly to the registered route function, they have to be defined before the route\nfunction that uses one is registered.\n```\n@app.parser("list")\ndef list_parser(value: str) -> list[str]:\n    return [value]\n\n\n@app.get("/users/{user_ids:list}")\ndef get_users(user_ids: list[str]) -> list[User]:\n    if not isinstance(user_ids, list) or len(user_ids) != 1:\n        raise AssertionError("Invalid input")\n    return list(map(User, user_ids))\n```\nA test case with this example is available in [tests/samples/test_with_custom_parser.py](tests/samples/test_with_custom_parser.py).\n\n## Resources\nAs your application grows, you may want to split certain functionality amongst resources and files, similar to\nblueprints of other popular frameworks like Flask.\n\nYou can define a resource like this:\n```\nfrom dataclasses import dataclass\n\nfrom vial.app import Resource, Vial\n\nstores_app = Resource(__name__)\n\n\n@dataclass\nclass Store:\n    store_id: str\n\n\n@stores_app.get("/stores/{store_id}")\ndef get_store(store_id: str) -> Store:\n    return Store(store_id)\n\n\napp = Vial(__name__)\n\napp.register_resource(stores_app)\n```\nA test case with this example is available in [tests/samples/test_with_resources.py](tests/samples/test_with_resources.py).\n\n## Middleware\nYou can register middleware functions to be executed before / after route invocations. All middleware is scoped to\nwhere it\'s registered. A middleware function registered with the `Vial` instance is scoped to all routes within\nthe application, but a function registered with a `Resource` instance will only be invoked for routes defined in\nthat specific resource.\n\nThe route invocation will be the last callable in the call chain, so any middleware in the chain before can\nexit the request and prevent the route invocation or even any other middleware from being called, exiting instead.\nThis may be helpful in cases where an application needs to fail early if the request isn\'t valid, like if a\nrequired header is missing.\n\nBelow is an example of registering a middleware to log route invocation:\n```\nfrom __future__ import annotations\n\nfrom vial import request\nfrom vial.app import Vial\nfrom vial.middleware import CallChain\nfrom vial.types import Request, Response\n\napp = Vial(__name__)\n\n\n@app.middleware\ndef log_events(event: Request, chain: CallChain) -> Response:\n    app.logger.info("Began execution of %s", event.context)\n    event.headers["custom-injected-header"] = "hello there"\n    try:\n        return chain(event)\n    finally:\n        app.logger.info("Completed execution of %s", event.context)\n\n\n@app.get("/hello-world")\ndef hello_world() -> dict[str, str | list[str]]:\n    return {"hello": "world", **request.get().headers}\n```\nA test case with this example is available in [tests/samples/test_with_middleware.py](tests/samples/test_with_middleware.py).\n\n\n## Error Handling\nWhen errors are raised by the application, the default error handler will iterate the class inheritance hierarchy of the\nexception that was raised, trying to find the most fine grained error handler possible. Default error handlers for common\nexception types like `Exception` or `ValueError` are provided, but can be overridden. Below is a sample on how to register\ncustom error handlers or override existing ones:\n```\nfrom http import HTTPStatus\n\nfrom vial.app import Vial\nfrom vial.gateway import Gateway\nfrom vial.types import Response\n\napp = Vial(__name__)\n\n\nclass CustomError(Exception):\n    pass\n\n\nclass ConfusedError(CustomError):\n    pass\n\n\n@app.error_handler(CustomError)\ndef custom_error_handler(error: CustomError) -> Response:\n    return Response({"custom_message": str(error)}, status=HTTPStatus.IM_A_TEAPOT)\n\n\n@app.error_handler(ConfusedError)\ndef confused_error_handler(error: ConfusedError) -> Response:\n    return Response({"custom_message": str(error)}, status=HTTPStatus.BAD_GATEWAY)\n\n\n@app.get("/teapot")\ndef teapot() -> None:\n    raise CustomError("I really am a teapot")\n\n\n@app.get("/confused-teapot")\ndef confused_teapot() -> None:\n    raise ConfusedError("I\'m a really confused teapot")\n```\nA test case with this example is available in [tests/samples/test_with_error_handling.py](tests/samples/test_with_error_handling.py).\n\nError handlers are bound to the resource they were registered in, whether that\'s the global `Vial` application or\na specific `Resource` instance. When an error occurs in a route, the "owner" application / resource is taken into consideration when\nchoosing the error handler to use.\n\nAn error handler registered in a `Resource` will always have precedence over a global error handler registered in the `Vial`\napplication. This allows for resources to either override global error handling mechanisms or add customization for more\nfine grained exception types.\n\nNote that the most fine grained error handler is always chosen, no matter where it comes from. That means that in a scenario\nlike this:\n```\nclass First(Exception):\n    pass\n\nclass Second(First):\n    pass\n\nclass Third(Second):\n    pass\n```\nIf the `Vial` application registers error handlers for `First` and `Third` while the `Resource` registers an override for\n`Second`, then when an exception of type `Third` is thrown, the global error handler will be used because it has a closer\nmatch to the exception, even if its parent is overridden in the `Resource`.\n\nBelow is an example of a `Resource` specific error handler:\n```\nfrom http import HTTPStatus\n\nfrom vial.app import Resource, Vial\nfrom vial.types import Response\n\napp = Vial(__name__)\n\nconfused_app = Resource(f"confused_{__name__}")\n\n\nclass CustomError(Exception):\n    pass\n\n\nclass ConfusedError(CustomError):\n    pass\n\n\n@app.error_handler(CustomError)\ndef custom_error_handler(error: CustomError) -> Response:\n    return Response({"custom_message": str(error)}, status=HTTPStatus.IM_A_TEAPOT)\n\n\n@confused_app.error_handler(ConfusedError)\ndef confused_error_handler(error: ConfusedError) -> Response:\n    return Response({"custom_message": str(error)}, status=HTTPStatus.BAD_GATEWAY)\n\n\n@app.get("/teapot")\ndef teapot() -> None:\n    raise CustomError("I really am a teapot")\n\n\n@confused_app.get("/confused-teapot")\ndef confused_teapot() -> None:\n    raise ConfusedError("I\'m a really confused teapot")\n\n\napp.register_resource(confused_app)\n```\nA test case with this example is available in [tests/samples/test_with_resource_error_handling.py](tests/samples/test_with_resource_error_handling.py).\n\n\n## Json Encoding\nYou can customize how Vial serializes / deserializes JSON objects by passing a custom encoder. The below\nexample shows how to substitute the native JSON module with another library like `simplejson`:\n```\nfrom decimal import Decimal\nfrom typing import Any\n\nimport simplejson\n\nfrom vial.app import Vial\nfrom vial.json import Json\n\n\nclass SimpleJson(Json):\n    @staticmethod\n    def dumps(value: Any) -> str:\n        return simplejson.dumps(value)\n\n    @staticmethod\n    def loads(value: str) -> Any:\n        return simplejson.loads(value)\n\n\nclass SimpleJsonVial(Vial):\n    json_class = SimpleJson\n\n\napp = SimpleJsonVial(__name__)\n\n\n@app.get("/prices")\ndef get_prices() -> dict[str, Decimal]:\n    # Decimal is not supported natively by the json module, but is by simplejson.\n    return {"bread": Decimal("42.24"), "cheese": Decimal("129.34")}\n```\nA test case with this example is available in [tests/samples/test_with_json_encoding.py](tests/samples/test_with_json_encoding.py).\n\n## Testing\nThe `vial.gateway.Gateway` class provides functionality to interact with the Vial application locally,\nwithout deploying to AWS Lambda. It can be constructed using the original `Vial` application instance,\nexposing the application endpoints with basic URL path matching.\n\nHere is an example test case using `pytest`:\n```\nfrom http import HTTPStatus\n\nimport pytest\n\nfrom vial import request\nfrom vial.app import Vial\nfrom vial.exceptions import BadRequestError\nfrom vial.gateway import Gateway\n\napp = Vial(__name__)\n\n\n@app.get("/health")\ndef health() -> dict[str, str]:\n    return {"status": "OK"}\n\n\n@app.post("/stores/{store_id}")\ndef create_store(store_id: str) -> dict[str, str]:\n    if not (body := request.get().body):\n        raise BadRequestError("Bad request")\n    return {"store_id": store_id, **app.json.loads(body)}\n\n\n@pytest.fixture(name="gateway")\ndef gateway_fixture() -> Gateway:\n    return Gateway(app)\n\n\ndef test_health(gateway: Gateway) -> None:\n    response = gateway.get("/health")\n    assert response.status == HTTPStatus.OK\n    assert response.body == {"status": "OK"}\n\n\ndef test_create_store(gateway: Gateway) -> None:\n    body = app.json.dumps({"store_name": "My cool store"})\n    response = gateway.post("/stores/my-cool-store", body)\n    assert response.status == HTTPStatus.OK\n    assert response.body == {"store_id": "my-cool-store", "store_name": "My cool store"}\n```\nThis code is also available in [tests/samples/test_with_gateway.py](tests/samples/test_with_gateway.py).\n',
    'author': 'Michael Dimchuk',
    'author_email': 'michaeldimchuk@gmail.com',
    'maintainer': 'Michael Dimchuk',
    'maintainer_email': 'michaeldimchuk@gmail.com',
    'url': 'https://github.com/michaeldimchuk/pyvial',
    'packages': packages,
    'package_data': package_data,
    'python_requires': '>=3.9,<4.0',
}


setup(**setup_kwargs)
