diff --git a/.gitignore b/.gitignore index d50861f..89711b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,13 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + # Distribution / packaging .Python -env/ build/ develop-eggs/ dist/ @@ -12,20 +19,155 @@ lib64/ parts/ sdist/ var/ +wheels/ +share/python-wheels/ *.egg-info/ .installed.cfg *.egg +MANIFEST +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec -.ruff_cache/ -__pycache__ +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock -.DS_Store +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments .env -.pytest_cache -.python-version +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ -package -*.zip -.mypy_cache -.coverage +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# PyPI configuration file +.pypirc + +.ruff_cache/ diff --git a/README.md b/README.md index 81a08ee..2c2a02e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,16 @@ # Smyth +
+ + [![docs](https://img.shields.io/badge/Docs-Smyth-f5c03b.svg?style=flat&logo=materialformkdocs)](https://mirumee.github.io/smyth/) ![pypi](https://img.shields.io/pypi/v/smyth?style=flat) ![licence](https://img.shields.io/pypi/l/smyth?style=flat) ![pypi downloads](https://img.shields.io/pypi/dm/smyth?style=flat) ![pyversion](https://img.shields.io/pypi/pyversions/smyth?style=flat) +
-Smyth is a versatile tool designed to enhance your AWS Lambda development experience. It is a pure Python tool that allows for easy customization and state persistence, making your Lambda development more efficient and developer-friendly. +Smyth is a tool designed to enhance your AWS Lambda development experience by mocking an AWS Lambda environment on your **local machine**. It is a pure Python tool that allows for easy customization and state persistence, making your Lambda development more efficient and developer-friendly. ## Features @@ -43,6 +47,9 @@ handler_path = "my_project.handlers.saleor.handler.saleor_http_handler" url_path = "/saleor/{path:path}" ``` +> [!TIP] +> Check the [documentation](https://mirumee.github.io/smyth/user_guide/all_settings/) for more configuration options. + Run Smyth with: ```bash python -m smyth @@ -75,11 +82,6 @@ To utilize the VS Code debugger with the Smyth tool, you can set up your `launch The combination of Uvicorn reload process and HTTP server process with what is being done with the Lambda processes is tricky. If a Lambda process is doing something and the HTTP server is killed in the wrong moment it's likely going to bork your terminal. This is not solved yet. It's best to use in a Docker container or have the ability to `kill -9 {PID of the Uvicorn reload process}` at hand. -## TODO - -- [ ] Write tests -- [x] Publish on PyPi - ## Name This name blends "Smith" (as in a blacksmith, someone who works in a forge) with "Py" for Python, altering the spelling to "Smyth". Blacksmiths are craftsmen who work with metal in a forge, shaping it into desired forms. Similarly, "Smyth" suggests a tool that helps developers craft and shape their serverless projects with the precision and skill of a smith, but in the realm of Python programming. This name retains the essence of craftsmanship and transformation inherent in a forge while being associated with Python. diff --git a/docs/user_guide/all_settings.md b/docs/user_guide/all_settings.md index 9ac29e6..b636e37 100644 --- a/docs/user_guide/all_settings.md +++ b/docs/user_guide/all_settings.md @@ -4,22 +4,24 @@ Here's a list of all the settings, including those that are simpler but equally ## Smyth Settings -### Host +### Socket Binding `host` - `str` (default: `"0.0.0.0"`) Used by Uvicorn to bind to an address. -### Port - `port` - `int` (default: `8080`) Used by Uvicorn as the bind port. -### Log Level +### Logging `log_level` - `str` (default: `"INFO"`) Sets the logging level for the `uvicorn` and `smyth` logging handlers. -### Smyth Path Prefix +### Smyth Internals `smyth_path_prefix` - `str` (default: `"/smyth"`) The path prefix used for Smyth's status endpoint. Change this if, for any reason, it collides with your path routing. +### Environment + +`env` - `dict[str, str]` (default: `{}`) Environment variables to apply to every handler. Read more about [environment variables here](environment.md). + ## Handler Settings ### Handler Path @@ -28,28 +30,47 @@ Here's a list of all the settings, including those that are simpler but equally ### URL Path -`url_path` - `str` (required) The Starlette routing path on which your handler will be exposed. +`url_path` - `str` (required) The [Starlette routing](https://www.starlette.io/routing/#http-routing) path on which your handler will be exposed. -### Timeout +### Environment -`timeout` - `float` (default: `None`, which means no timeout) The time in seconds after which the Lambda Handler raises a Timeout Exception, simulating Lambda's real-life timeouts. +`env` - `dict[str, str]` (default: `{}`) Environment variables to apply to this handler - keys defined here take precedence over the ones defined in `tool.smyth.env` and be otherwise merged. Read more about [environment variables here](environment.md). -### Event Data Function +### Customization `event_data_function_path` - `str` (default: `"smyth.event.generate_api_gw_v2_event_data"`) Read more about [event functions here](event_functions.md). -### Context Data Function - `context_data_function_path` - `str` (default: `"smyth.context.generate_context_data"`) A function similar to the [event generator](event_functions.md), but it constructs the `context`, adding some metadata from Smyth's runtime. You can create and use your own. -### Log Level - -`log_level` - `str` (default: `"INFO"`) Log level for Smyth's runner function, which is still part of Smyth but already running in the subprocess. Note that the logging of your Lambda handler code should be set separately. +### Behaviour -### Concurrency +`timeout` - `float` (default: `None`, which means no timeout) The time in seconds after which the Lambda Handler raises a Timeout Exception, simulating Lambda's real-life timeouts. `concurrency` - `int` (default: `1`) Read more about [concurrency here](concurrency.md). -### Strategy Generator - `strategy_generator_path` - `str` (default: `"smyth.runner.strategy.first_warm"`) Read more about [dispatch strategies here](concurrency.md/#dispatch-strategy). + +### Logging + +`log_level` - `str` (default: `"INFO"`) Log level for Smyth's runner function, which is still part of Smyth but already running in the subprocess. Note that the logging of your Lambda handler code should be set separately. + + +## `pyproject.toml` example + +```toml title='pyproject.toml' linenums="1" +[tool.smyth] +host = "0.0.0.0" +port = 8080 +log_level = "INFO" +smyth_path_prefix = "/smyth" + +[tool.smyth.handlers.lambda_handler] +handler_path = "myproject.app.lambda_handler" +url_path = "{path:path}" +timeout = 300 +event_data_function_path = "smyth.event.generate_api_gw_v2_event_data" +context_data_function_path = "smyth.context.generate_context_data" +log_level = "DEBUG" +concurrency = 3 +strategy_generator_path = "smyth.runner.strategy.first_warm" +``` diff --git a/docs/user_guide/custom_entrypoint.md b/docs/user_guide/custom_entrypoint.md index a7bf3a1..bd741ef 100644 --- a/docs/user_guide/custom_entrypoint.md +++ b/docs/user_guide/custom_entrypoint.md @@ -26,13 +26,14 @@ Here's an example `smyth_conf.py` file: import uvicorn from smyth.server.app import SmythStarlette from smyth.smyth import Smyth +from smyth.types import EventData, RunnerProcessProtocol, SmythHandler from starlette.requests import Request def my_handler(event, context): return {"statusCode": 200, "body": "Hello, World!"} -async def my_event_data_generator(request: Request): +async def my_event_data_generator(request: Request, smyth_handler: SmythHandler, process: RunnerProcessProtocol) -> EventData: return { "requestContext": { "http": { diff --git a/docs/user_guide/environment.md b/docs/user_guide/environment.md new file mode 100644 index 0000000..207b4d9 --- /dev/null +++ b/docs/user_guide/environment.md @@ -0,0 +1,61 @@ +# Environment + +Smyth allows you to overwrite the environemnt variables that are passed to the handlers to better reflect the actual AWS Lambda environment while allowing you to change things while developing locally. + + +```toml title='pyproject.toml' linenums="1" +[tool.smyth] +host = "0.0.0.0" +port = 8080 +... + +[tool.smyth.env] +AWS_ENDPOINT = "http://localstack:4566" +AWS_LAMBDA_FUNCTION_VERSION = "$SMYTH" + +[tool.smyth.handlers.my_special_version_handler] +handler_path = "mypyoject.app.my_special_version_handler" +url_path = "{path:path}" +... + +[tool.smyth.handlers.my_special_version_handler.env] +AWS_LAMBDA_FUNCTION_VERSION = "34" +``` + +The config above allows you to set a specific env var for every defined handler and overwrite or set +specific values for individual handlers. In the example every handler would receive the +`AWS_ENDPOINT = "http://localstack:4566"` and `AWS_LAMBDA_FUNCTION_VERSION = "$SMYTH"` env vars with +the exception of `my_special_version_handler` which will have a different version. + +## Fake context + +The `smyth.runner.fake_context.FakeLambdaContext` class used by Smyth will also consume of of the environment variables. + +## Default variables + +In the table bellow you will find which keys are set by Smyth when a handler is being invoked. +Smyth will look for the key in the following order: + +1. the handler configuration `env` key +2. the smyth global configuration `env` key +3. `os.environ` +4. when none of the above contain the key the default value is assigned + +| Key | Default Value | +| ----------------------------------- | ---------------------------------------------------------------------- | +| `"_HANDLER"` | `self.lambda_handler_path` | +| `"AWS_ACCESS_KEY_ID"` | `"000000000000"` | +| `"AWS_SECRET_ACCESS_KEY"` | `"test"` | +| `"AWS_SESSION_TOKEN"` | `"test"` | +| `"AWS_DEFAULT_REGION"` | `"eu-central-1"` | +| `"AWS_REGION"` | `"eu-central-1"` | +| `"AWS_EXECUTION_ENV"` | `"AWS_Lambda_python{sys.version_info.major}.{sys.version_info.minor}"` | +| `"AWS_LAMBDA_FUNCTION_MEMORY_SIZE"` | `"128"` | +| `"AWS_LAMBDA_FUNCTION_NAME"` | `self.name` | +| `"AWS_LAMBDA_FUNCTION_VERSION"` | `"$LATEST"` | +| `"AWS_LAMBDA_INITIALIZATION_TYPE"` | `"on-demand"` | +| `"AWS_LAMBDA_LOG_GROUP_NAME"` | `"/aws/lambda/{self.name}"` | +| `"AWS_LAMBDA_LOG_STREAM_NAME"` | `"{strftime('%Y/%m/%d')}/[$LATEST]smyth_aws_lambda_log_stream_name"` | +| `"AWS_LAMBDA_RUNTIME_API"` | `"127.0.0.1:9001"` | +| `"AWS_XRAY_CONTEXT_MISSING"` | `"LOG_ERROR"` | +| `"AWS_XRAY_DAEMON_ADDRESS"` | `"127.0.0.1:2000"` | diff --git a/docs/user_guide/event_functions.md b/docs/user_guide/event_functions.md index 0330749..96dfa56 100644 --- a/docs/user_guide/event_functions.md +++ b/docs/user_guide/event_functions.md @@ -13,7 +13,10 @@ The first one builds a minimal API Gateway Proxy V2 event to simulate a Lambda b If you need to work with events not covered by Smyth, you can create and provide your own. Assuming a simplified API Gateway V1 event, you can create a generator like this: ```python title="my_project/src/smyth_utils/event.py" linenums="1" -async def generate_api_gw_v1_event_data(request: Request): +from smyth.types import EventData, RunnerProcessProtocol, SmythHandler + + +async def generate_api_gw_v1_event_data(request: Request, smyth_handler: SmythHandler, process: RunnerProcessProtocol) -> EventData: source_ip = None if request.client: source_ip = request.client.host diff --git a/docs/user_guide/index.md b/docs/user_guide/index.md index 2f3e648..b51a0ac 100644 --- a/docs/user_guide/index.md +++ b/docs/user_guide/index.md @@ -1,4 +1,4 @@ -# User Guide +# First Steps Smyth is built to have minimal or no impact on the project you are working on. That said, it comes with features that allow you to customize Smyth to the needs of your Lambda project. diff --git a/mkdocs.yml b/mkdocs.yml index 678ba43..dfc115d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,7 +18,6 @@ theme: - content.code.copy - content.code.annotate - content.tabs.link - - navigation.indexes - navigation.footer - navigation.tracking - navigation.expand @@ -31,6 +30,7 @@ nav: - user_guide/event_functions.md - user_guide/invoke.md - user_guide/concurrency.md + - user_guide/environment.md - user_guide/all_settings.md - user_guide/custom_entrypoint.md - user_guide/non_http.md diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index a2acf80..0000000 --- a/poetry.lock +++ /dev/null @@ -1,712 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand. - -[[package]] -name = "annotated-types" -version = "0.6.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -files = [ - {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, - {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, -] - -[[package]] -name = "anyio" -version = "4.3.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.8" -files = [ - {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, - {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, -] - -[package.dependencies] -idna = ">=2.8" -sniffio = ">=1.1" - -[package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.23)"] - -[[package]] -name = "asgiref" -version = "3.7.2" -description = "ASGI specs, helper code, and adapters" -optional = false -python-versions = ">=3.7" -files = [ - {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, - {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, -] - -[package.extras] -tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] - -[[package]] -name = "asttokens" -version = "2.4.1" -description = "Annotate AST trees with source code positions" -optional = false -python-versions = "*" -files = [ - {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, - {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, -] - -[package.dependencies] -six = ">=1.12.0" - -[package.extras] -astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] -test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] - -[[package]] -name = "aws-lambda-powertools" -version = "2.34.1" -description = "Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity." -optional = false -python-versions = ">=3.8,<4.0.0" -files = [ - {file = "aws_lambda_powertools-2.34.1-py3-none-any.whl", hash = "sha256:5fea5d6ae48283db81c43ef881d113d4a9c312babe3340a42fc5bc9349fbdbf6"}, - {file = "aws_lambda_powertools-2.34.1.tar.gz", hash = "sha256:4912e2a7450d9b7a7c289d54f56d5afe0b0141013f745989359cfadd489ef553"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.2,<5.0.0" - -[package.extras] -all = ["aws-encryption-sdk (>=3.1.1,<4.0.0)", "aws-xray-sdk (>=2.8.0,<3.0.0)", "fastjsonschema (>=2.14.5,<3.0.0)", "jsonpath-ng (>=1.6.0,<2.0.0)", "pydantic (>=1.8.2,<2.0.0)"] -aws-sdk = ["boto3 (>=1.26.164,<2.0.0)"] -datadog = ["datadog-lambda (>=4.77,<6.0)"] -datamasking = ["aws-encryption-sdk (>=3.1.1,<4.0.0)", "jsonpath-ng (>=1.6.0,<2.0.0)"] -parser = ["pydantic (>=1.8.2,<2.0.0)"] -redis = ["redis (>=4.4,<6.0)"] -tracer = ["aws-xray-sdk (>=2.8.0,<3.0.0)"] -validation = ["fastjsonschema (>=2.14.5,<3.0.0)"] - -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "decorator" -version = "5.1.1" -description = "Decorators for Humans" -optional = false -python-versions = ">=3.5" -files = [ - {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, - {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, -] - -[[package]] -name = "executing" -version = "2.0.1" -description = "Get the currently executing AST node of a frame, and other information" -optional = false -python-versions = ">=3.5" -files = [ - {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, - {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, -] - -[package.extras] -tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] - -[[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.7" -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] - -[[package]] -name = "idna" -version = "3.6" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.5" -files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, -] - -[[package]] -name = "ipdb" -version = "0.13.13" -description = "IPython-enabled pdb" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "ipdb-0.13.13-py3-none-any.whl", hash = "sha256:45529994741c4ab6d2388bfa5d7b725c2cf7fe9deffabdb8a6113aa5ed449ed4"}, - {file = "ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726"}, -] - -[package.dependencies] -decorator = {version = "*", markers = "python_version >= \"3.11\""} -ipython = {version = ">=7.31.1", markers = "python_version >= \"3.11\""} - -[[package]] -name = "ipython" -version = "8.22.1" -description = "IPython: Productive Interactive Computing" -optional = false -python-versions = ">=3.10" -files = [ - {file = "ipython-8.22.1-py3-none-any.whl", hash = "sha256:869335e8cded62ffb6fac8928e5287a05433d6462e3ebaac25f4216474dd6bc4"}, - {file = "ipython-8.22.1.tar.gz", hash = "sha256:39c6f9efc079fb19bfb0f17eee903978fe9a290b1b82d68196c641cecb76ea22"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -decorator = "*" -jedi = ">=0.16" -matplotlib-inline = "*" -pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} -prompt-toolkit = ">=3.0.41,<3.1.0" -pygments = ">=2.4.0" -stack-data = "*" -traitlets = ">=5.13.0" - -[package.extras] -all = ["ipython[black,doc,kernel,nbconvert,nbformat,notebook,parallel,qtconsole,terminal]", "ipython[test,test-extra]"] -black = ["black"] -doc = ["docrepr", "exceptiongroup", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "stack-data", "typing-extensions"] -kernel = ["ipykernel"] -nbconvert = ["nbconvert"] -nbformat = ["nbformat"] -notebook = ["ipywidgets", "notebook"] -parallel = ["ipyparallel"] -qtconsole = ["qtconsole"] -test = ["pickleshare", "pytest (<8)", "pytest-asyncio (<0.22)", "testpath"] -test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] - -[[package]] -name = "jedi" -version = "0.19.1" -description = "An autocompletion tool for Python that can be used for text editors." -optional = false -python-versions = ">=3.6" -files = [ - {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, - {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, -] - -[package.dependencies] -parso = ">=0.8.3,<0.9.0" - -[package.extras] -docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] -qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] -testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.8" -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -code-style = ["pre-commit (>=3.0,<4.0)"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins"] -profiling = ["gprof2dot"] -rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "matplotlib-inline" -version = "0.1.6" -description = "Inline Matplotlib backend for Jupyter" -optional = false -python-versions = ">=3.5" -files = [ - {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, - {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, -] - -[package.dependencies] -traitlets = "*" - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "mypy" -version = "1.8.0" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, - {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, - {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, - {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, - {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, - {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, - {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, - {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, - {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, - {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, - {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, - {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, - {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, - {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, - {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, - {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, - {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, - {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, - {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, - {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, - {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, - {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, - {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, - {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, - {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, - {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, - {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, -] - -[package.dependencies] -mypy-extensions = ">=1.0.0" -typing-extensions = ">=4.1.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "parso" -version = "0.8.3" -description = "A Python Parser" -optional = false -python-versions = ">=3.6" -files = [ - {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, - {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, -] - -[package.extras] -qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] -testing = ["docopt", "pytest (<6.0.0)"] - -[[package]] -name = "pexpect" -version = "4.9.0" -description = "Pexpect allows easy control of interactive console applications." -optional = false -python-versions = "*" -files = [ - {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, - {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, -] - -[package.dependencies] -ptyprocess = ">=0.5" - -[[package]] -name = "prompt-toolkit" -version = "3.0.43" -description = "Library for building powerful interactive command lines in Python" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"}, - {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"}, -] - -[package.dependencies] -wcwidth = "*" - -[[package]] -name = "ptyprocess" -version = "0.7.0" -description = "Run a subprocess in a pseudo terminal" -optional = false -python-versions = "*" -files = [ - {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, - {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, -] - -[[package]] -name = "pure-eval" -version = "0.2.2" -description = "Safely evaluate AST nodes without side effects" -optional = false -python-versions = "*" -files = [ - {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, - {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, -] - -[package.extras] -tests = ["pytest"] - -[[package]] -name = "pydantic" -version = "2.6.2" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic-2.6.2-py3-none-any.whl", hash = "sha256:37a5432e54b12fecaa1049c5195f3d860a10e01bdfd24f1840ef14bd0d3aeab3"}, - {file = "pydantic-2.6.2.tar.gz", hash = "sha256:a09be1c3d28f3abe37f8a78af58284b236a92ce520105ddc91a6d29ea1176ba7"}, -] - -[package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.16.3" -typing-extensions = ">=4.6.1" - -[package.extras] -email = ["email-validator (>=2.0.0)"] - -[[package]] -name = "pydantic-core" -version = "2.16.3" -description = "" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, - {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, - {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, - {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, - {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, - {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, - {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, - {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, - {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, - {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, - {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, - {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, - {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, - {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, - {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, - {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, - {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, - {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, - {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, - {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, - {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, - {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, - {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, - {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, - {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, - {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, - {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, - {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, - {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, - {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, - {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, - {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, - {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - -[[package]] -name = "pygments" -version = "2.17.2" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.7" -files = [ - {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, - {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, -] - -[package.extras] -plugins = ["importlib-metadata"] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "rich" -version = "13.7.0" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, - {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "ruff" -version = "0.2.2" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -files = [ - {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"}, - {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"}, - {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"}, - {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"}, - {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"}, - {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, -] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "sniffio" -version = "1.3.0" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, -] - -[[package]] -name = "stack-data" -version = "0.6.3" -description = "Extract data from python stack frames and tracebacks for informative displays" -optional = false -python-versions = "*" -files = [ - {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, - {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, -] - -[package.dependencies] -asttokens = ">=2.1.0" -executing = ">=1.2.0" -pure-eval = "*" - -[package.extras] -tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] - -[[package]] -name = "starlette" -version = "0.37.1" -description = "The little ASGI library that shines." -optional = false -python-versions = ">=3.8" -files = [ - {file = "starlette-0.37.1-py3-none-any.whl", hash = "sha256:92a816002d4e8c552477b089520e3085bb632e854eb32cef99acb6f6f7830b69"}, - {file = "starlette-0.37.1.tar.gz", hash = "sha256:345cfd562236b557e76a045715ac66fdc355a1e7e617b087834a76a87dcc6533"}, -] - -[package.dependencies] -anyio = ">=3.4.0,<5" - -[package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] - -[[package]] -name = "traitlets" -version = "5.14.1" -description = "Traitlets Python configuration system" -optional = false -python-versions = ">=3.8" -files = [ - {file = "traitlets-5.14.1-py3-none-any.whl", hash = "sha256:2e5a030e6eff91737c643231bfcf04a65b0132078dad75e4936700b213652e74"}, - {file = "traitlets-5.14.1.tar.gz", hash = "sha256:8585105b371a04b8316a43d5ce29c098575c2e477850b62b848b964f1444527e"}, -] - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] - -[[package]] -name = "types-toml" -version = "0.10.8.7" -description = "Typing stubs for toml" -optional = false -python-versions = "*" -files = [ - {file = "types-toml-0.10.8.7.tar.gz", hash = "sha256:58b0781c681e671ff0b5c0319309910689f4ab40e8a2431e205d70c94bb6efb1"}, - {file = "types_toml-0.10.8.7-py3-none-any.whl", hash = "sha256:61951da6ad410794c97bec035d59376ce1cbf4453dc9b6f90477e81e4442d631"}, -] - -[[package]] -name = "typing-extensions" -version = "4.9.0" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, - {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, -] - -[[package]] -name = "uvicorn" -version = "0.27.1" -description = "The lightning-fast ASGI server." -optional = false -python-versions = ">=3.8" -files = [ - {file = "uvicorn-0.27.1-py3-none-any.whl", hash = "sha256:5c89da2f3895767472a35556e539fd59f7edbe9b1e9c0e1c99eebeadc61838e4"}, - {file = "uvicorn-0.27.1.tar.gz", hash = "sha256:3d9a267296243532db80c83a959a3400502165ade2c1338dea4e67915fd4745a"}, -] - -[package.dependencies] -click = ">=7.0" -h11 = ">=0.8" - -[package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] - -[[package]] -name = "wcwidth" -version = "0.2.13" -description = "Measures the displayed width of unicode strings in a terminal" -optional = false -python-versions = "*" -files = [ - {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, - {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, -] - -[metadata] -lock-version = "2.0" -python-versions = "^3.12" -content-hash = "607c245e8c6767a9a299b2ca7c18da6581ae1447de77a5deaa92348ff13f2422" diff --git a/pyproject.toml b/pyproject.toml index 45d04cc..d30de4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,8 @@ dynamic = ["version"] description = "Smyth is a versatile tool designed to enhance your AWS Lambda development experience. It is a pure Python tool that allows for easy customization and state persistence, making your Lambda development more efficient and developer-friendly." readme = "README.md" requires-python = ">=3.10" -license = { file = "LICENSE.txt" } +license = "BSD-3-Clause" +license-files = ["LICENSE.txt"] keywords = [] authors = [{ name = "Mirumee", email = "hello@mirumee.com" }] classifiers = [ @@ -47,7 +48,7 @@ Source = "https://github.com/mirumee/smyth" [project.optional-dependencies] dev = ["ipdb"] types = ["mypy>=1.0.0", "pytest", "types-toml", "pytest-asyncio"] -docs = ["mkdocs-material", "termynal"] +docs = ["mkdocs-material~=9.0", "termynal"] [tool.hatch.version] path = "src/smyth/__about__.py" @@ -65,9 +66,10 @@ features = ["dev", "types", "docs"] [tool.hatch.envs.default.scripts] check = [ "hatch fmt", - "hatch test -a", + "hatch test -a -p", "hatch test --cover", "hatch run types:check", + "hatch run docs:build", ] ## Types environment @@ -75,32 +77,19 @@ check = [ [tool.hatch.envs.types.scripts] check = "mypy --install-types --non-interactive {args:src/smyth}" -[tool.mypy] -check_untyped_defs = true - -[[tool.mypy.overrides]] -module = "setproctitle.*" -ignore_missing_imports = true - - ## Test environment [tool.hatch.envs.hatch-test] -dependencies = [ - "asynctest", +extra-dependencies = [ "ipdb", "anyio", - "pytest-mock", - "pytest-memray", + "pytest-freezer", "pytest-print", "pytest-cov", - "coverage[toml]", "httpx", - # uvloop 0.20.0 is broken on Python 3.13 - # https://github.com/MagicStack/uvloop/issues/622 - # waiting for 0.21.0 release - "uvloop==0.21.0b1", + "uvloop>=0.21", ] +extra-args = [] [[tool.hatch.envs.hatch-test.matrix]] python = ["3.10", "3.11", "3.12", "3.13"] @@ -118,6 +107,25 @@ deploy = "mkdocs gh-deploy --force" [tool.pytest.ini_options] +## Types configuration + +[tool.mypy] +python_version = "3.10" +files = ["src/**/*.py"] +exclude = "tests/.*" +warn_redundant_casts = true +warn_unused_ignores = true +disallow_any_generics = true +check_untyped_defs = true +no_implicit_reexport = true +disallow_untyped_defs = true +strict = true +disable_error_code = ["import-untyped"] + +[[tool.mypy.overrides]] +module = "setproctitle.*" +ignore_missing_imports = true + ## Coverage configuration [tool.coverage.run] diff --git a/src/smyth/__main__.py b/src/smyth/__main__.py index e5e4f91..1f9469f 100644 --- a/src/smyth/__main__.py +++ b/src/smyth/__main__.py @@ -50,7 +50,7 @@ def run( quiet: Annotated[ bool, typer.Option(help="Effectively the same as --log-level=ERROR") ] = False, -): +) -> None: if host: config.host = host if port: diff --git a/src/smyth/config.py b/src/smyth/config.py index 3bfbf2b..2fccba3 100644 --- a/src/smyth/config.py +++ b/src/smyth/config.py @@ -1,11 +1,14 @@ import json import os +from copy import deepcopy from dataclasses import asdict, dataclass, field from pathlib import Path +from typing import Any import toml from smyth.exceptions import ConfigFileNotFoundError +from smyth.types import Environ @dataclass @@ -18,6 +21,12 @@ class HandlerConfig: log_level: str = "DEBUG" concurrency: int = 1 strategy_generator_path: str = "smyth.runner.strategy.first_warm" + env: Environ = field(default_factory=dict) + + def get_env_overrides(self, config: "Config") -> Environ: + env = deepcopy(config.env) + env.update(self.env) + return env @dataclass @@ -27,9 +36,10 @@ class Config: handlers: dict[str, HandlerConfig] = field(default_factory=dict) log_level: str = "INFO" smyth_path_prefix: str = "/smyth" + env: Environ = field(default_factory=dict) @classmethod - def from_dict(cls, config_dict: dict): + def from_dict(cls, config_dict: dict[str, Any]) -> "Config": handler_data = config_dict.pop("handlers") handlers = { handler_name: HandlerConfig(**handler_config) @@ -48,7 +58,7 @@ def get_config_file_path(file_name: str = "pyproject.toml") -> Path: return directory.joinpath(file_name).resolve() -def get_config_dict(config_file_name: str | None = None) -> dict: +def get_config_dict(config_file_name: str | None = None) -> dict[str, Any]: """Get config dict.""" if config_file_name: config_file_path = get_config_file_path(config_file_name) @@ -58,7 +68,7 @@ def get_config_dict(config_file_name: str | None = None) -> dict: return toml.load(config_file_path) -def get_config(config_dict: dict) -> Config: +def get_config(config_dict: dict[str, Any]) -> Config: """Get config.""" if environ_config := os.environ.get("__SMYTH_CONFIG"): config_data = json.loads(environ_config) diff --git a/src/smyth/context.py b/src/smyth/context.py index 2432814..f5ef6df 100644 --- a/src/smyth/context.py +++ b/src/smyth/context.py @@ -3,12 +3,12 @@ from starlette.requests import Request -from smyth.types import RunnerProcessProtocol, SmythHandler +from smyth.types import ContextData, RunnerProcessProtocol, SmythHandler async def generate_context_data( request: Request | None, smyth_handler: SmythHandler, process: RunnerProcessProtocol -): +) -> ContextData: """ The data returned by this function is passed to the `smyth.runner.FaneContext` as kwargs. diff --git a/src/smyth/event.py b/src/smyth/event.py index 7ae24bc..c7da74d 100644 --- a/src/smyth/event.py +++ b/src/smyth/event.py @@ -1,7 +1,13 @@ +from typing import Any + from starlette.requests import Request +from smyth.types import EventData, RunnerProcessProtocol, SmythHandler + -async def generate_api_gw_v2_event_data(request: Request): +async def generate_api_gw_v2_event_data( + request: Request, smyth_handler: SmythHandler, process: RunnerProcessProtocol +) -> EventData: source_ip = None if request.client: source_ip = request.client.host @@ -28,5 +34,7 @@ async def generate_api_gw_v2_event_data(request: Request): } -async def generate_lambda_invocation_event_data(request: Request): +async def generate_lambda_invocation_event_data( + request: Request, smyth_handler: SmythHandler, process: RunnerProcessProtocol +) -> Any: return await request.json() diff --git a/src/smyth/runner/fake_context.py b/src/smyth/runner/fake_context.py index 276f64c..031d17c 100644 --- a/src/smyth/runner/fake_context.py +++ b/src/smyth/runner/fake_context.py @@ -1,5 +1,8 @@ +import os import sys +from collections.abc import Callable from time import strftime, time +from typing import Any from aws_lambda_powertools.utilities.typing import LambdaContext @@ -8,23 +11,23 @@ class FakeLambdaContext(LambdaContext): def __init__( self, name: str | None = None, - version: str | None = "LATEST", + version: str | None = None, timeout: int | None = None, - **kwargs, + **kwargs: Any, ): if name is None: - name = "Fake" - self.name = name + name = os.environ.get("AWS_LAMBDA_FUNCTION_NAME", "Fake") + self._name = name if version is None: - version = "LATEST" - self.version = version + version = os.environ.get("AWS_LAMBDA_FUNCTION_VERSION", "$LATEST") + self._version = version - self.created = time() + self._created = time() if timeout is None: timeout = 6 - self.timeout = timeout + self._timeout = timeout for key, value in kwargs.items(): setattr(self, key, value) @@ -32,45 +35,44 @@ def __init__( def get_remaining_time_in_millis(self) -> int: # type: ignore[override] return int( max( - (self.timeout * 1000) - - (int(round(time() * 1000)) - int(round(self.created * 1000))), + (self._timeout * 1000) + - (int(round(time() * 1000)) - int(round(self._created * 1000))), 0, ) ) @property - def function_name(self): - return self.name + def function_name(self) -> str: + return self._name @property - def function_version(self): - return self.version + def function_version(self) -> str: + return self._version @property - def invoked_function_arn(self): - return "arn:aws:lambda:serverless:" + self.name + def invoked_function_arn(self) -> str: + return "arn:aws:lambda:serverless:" + self._name @property - def memory_limit_in_mb(self): - return "1024" + # This indeed is a string in the real context hence the ignore[override] + def memory_limit_in_mb(self) -> str: # type: ignore[override] + return os.environ.get("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "128") @property - def aws_request_id(self): + def aws_request_id(self) -> str: return "1234567890" @property - def log_group_name(self): - return "/aws/lambda/" + self.name + def log_group_name(self) -> str: + return os.environ.get("AWS_LAMBDA_LOG_GROUP_NAME", f"/aws/lambda/{self._name}") @property - def log_stream_name(self): - return ( - strftime("%Y/%m/%d") - + "/[$" - + self.version - + "]58419525dade4d17a495dceeeed44708" + def log_stream_name(self) -> str: + return os.environ.get( + "AWS_LAMBDA_LOG_STREAM_NAME", + f"{strftime('%Y/%m/%d')}/[{self._version}]smyth_aws_lambda_log_stream_name", ) @property - def log(self): + def log(self) -> Callable[[str], int] | Any: return sys.stdout.write diff --git a/src/smyth/runner/process.py b/src/smyth/runner/process.py index b4df41a..8b2e9c3 100644 --- a/src/smyth/runner/process.py +++ b/src/smyth/runner/process.py @@ -1,12 +1,15 @@ import inspect import logging import logging.config +import os import signal import sys import traceback +from collections.abc import Generator from multiprocessing import Process, Queue, set_start_method from queue import Empty from time import time +from types import FrameType from asgiref.sync import sync_to_async from setproctitle import setproctitle @@ -18,7 +21,18 @@ SubprocessError, ) from smyth.runner.fake_context import FakeLambdaContext -from smyth.types import LambdaHandler, RunnerMessage, SmythHandlerState +from smyth.types import ( + EventData, + LambdaErrorResponse, + LambdaHandler, + LambdaResponse, + RunnerErrorMessage, + RunnerInputMessage, + RunnerOutputMessage, + RunnerResponseMessage, + RunnerStatusMessage, + SmythHandlerState, +) from smyth.utils import get_logging_config, import_attribute set_start_method("spawn", force=True) @@ -31,14 +45,28 @@ class RunnerProcess(Process): last_used_timestamp: float state: SmythHandlerState - def __init__(self, name: str, lambda_handler_path: str, log_level: str = "INFO"): + def __init__( + self, + name: str, + lambda_handler_path: str, + log_level: str = "INFO", + environ_override: dict[str, str] | None = None, + ): self.name = name self.task_counter = 0 self.last_used_timestamp = 0 self.state = SmythHandlerState.COLD + self.environ_override = environ_override + + self.environ: dict[str, str] = { + "_HANDLER": lambda_handler_path, + } + + if environ_override: + self.environ.update(environ_override) - self.input_queue: Queue[RunnerMessage] = Queue(maxsize=1) - self.output_queue: Queue[RunnerMessage] = Queue(maxsize=1) + self.input_queue: Queue[RunnerInputMessage] = Queue(maxsize=1) + self.output_queue: Queue[RunnerOutputMessage] = Queue(maxsize=1) self.lambda_handler_path = lambda_handler_path self.log_level = log_level @@ -46,15 +74,15 @@ def __init__(self, name: str, lambda_handler_path: str, log_level: str = "INFO") name=name, ) - def stop(self): - self.input_queue.put({"type": "smyth.stop"}) + def stop(self) -> None: + self.input_queue.put(RunnerInputMessage(type="smyth.stop")) self.join() self.input_queue.close() self.output_queue.close() self.input_queue.join_thread() self.output_queue.join_thread() - def send(self, data) -> RunnerMessage | None: + def send(self, data: RunnerInputMessage) -> LambdaResponse | None: LOGGER.debug("Sending data to process %s: %s", self.name, data) self.task_counter += 1 self.last_used_timestamp = time() @@ -78,30 +106,32 @@ def send(self, data) -> RunnerMessage | None: return None LOGGER.debug("Received message from process %s: %s", self.name, message) - if message["type"] == "smyth.lambda.status": - self.state = SmythHandlerState(message["status"]) - elif message["type"] == "smyth.lambda.response": + + if message.type == "smyth.lambda.status": + self.state = message.status + elif message.type == "smyth.lambda.response": self.state = SmythHandlerState.WARM - return message["response"] - elif message["type"] == "smyth.lambda.error": + return message.response + elif message.type == "smyth.lambda.error": self.state = SmythHandlerState.WARM - if message["response"]["type"] == "LambdaTimeoutError": - raise LambdaTimeoutError(message["response"]["message"]) + if message.error.type == "LambdaTimeoutError": + raise LambdaTimeoutError(message.error.message) else: - raise LambdaInvocationError(message["response"]["message"]) + raise LambdaInvocationError(message.error.message) @sync_to_async(thread_sensitive=False) - def asend(self, data) -> RunnerMessage | None: + def asend(self, data: RunnerInputMessage) -> LambdaResponse | None: return self.send(data) # Backend - def run(self): + def run(self) -> None: setproctitle(f"smyth:{self.name}") logging.config.dictConfig(get_logging_config(self.log_level)) + os.environ.update(self.environ) self.lambda_invoker__() - def get_message__(self): + def get_message__(self) -> Generator[RunnerInputMessage, None, None]: while True: try: message = self.input_queue.get(block=True, timeout=1) @@ -112,21 +142,27 @@ def get_message__(self): continue else: LOGGER.debug("Received message: %s", message) - if message["type"] == "smyth.stop": + if message.type == "smyth.stop": LOGGER.debug("Stopping process") return yield message - def get_event__(self, message): - return message["event"] + def get_event__(self, message: RunnerInputMessage) -> EventData: + if message.event is None: + raise LambdaInvocationError("No event data provided") + return message.event - def get_context__(self, message): - return FakeLambdaContext(**message["context"]) + def get_context__(self, message: RunnerInputMessage) -> FakeLambdaContext: + if message.context is None: + raise LambdaInvocationError("No context data provided") + return FakeLambdaContext(**message.context) - def import_handler__(self, lambda_handler_path, event, context): + def import_handler__( + self, lambda_handler_path: str, event: EventData, context: FakeLambdaContext + ) -> LambdaHandler: LOGGER.info("Starting cold, importing '%s'", lambda_handler_path) try: - handler = import_attribute(lambda_handler_path) + handler: LambdaHandler = import_attribute(lambda_handler_path) except ImportError as error: raise LambdaHandlerLoadError( f"Error importing handler: {error}, module not found" @@ -146,21 +182,23 @@ def import_handler__(self, lambda_handler_path, event, context): ) return handler - def set_status__(self, status: SmythHandlerState): - self.output_queue.put({"type": "smyth.lambda.status", "status": status}) + def set_status__(self, status: SmythHandlerState) -> None: + self.output_queue.put( + RunnerStatusMessage(type="smyth.lambda.status", status=status) + ) @staticmethod - def timeout_handler__(signum, frame): + def timeout_handler__(signum: int, frame: FrameType | None) -> None: raise LambdaTimeoutError("Lambda timeout") - def lambda_invoker__(self): + def lambda_invoker__(self) -> None: sys.stdin = open("/dev/stdin") lambda_handler: LambdaHandler | None = None self.set_status__(SmythHandlerState.COLD) for message in self.get_message__(): - if message.get("type") != "smyth.lambda.invoke": - LOGGER.error("Invalid message type: %s", message.get("type")) + if message.type != "smyth.lambda.invoke": + LOGGER.error("Invalid message type: %s", message.type) continue event = self.get_event__(message) @@ -175,7 +213,7 @@ def lambda_invoker__(self): self.set_status__(SmythHandlerState.WARM) signal.signal(signal.SIGALRM, self.timeout_handler__) - signal.alarm(int(context.timeout)) + signal.alarm(int(context._timeout)) self.set_status__(SmythHandlerState.WORKING) try: response = lambda_handler(event, context) @@ -186,21 +224,21 @@ def lambda_invoker__(self): extra={"log_setting": "console_full_width"}, ) self.output_queue.put( - { - "type": "smyth.lambda.error", - "response": { - "type": type(error).__name__, - "message": str(error), - "stacktrace": traceback.format_exc(), - }, - } + RunnerErrorMessage( + type="smyth.lambda.error", + error=LambdaErrorResponse( + type=type(error).__name__, + message=str(error), + stacktrace=traceback.format_exc(), + ), + ) ) else: self.output_queue.put( - { - "type": "smyth.lambda.response", - "response": response, - } + RunnerResponseMessage( + type="smyth.lambda.response", + response=response, + ) ) finally: signal.alarm(0) diff --git a/src/smyth/server/app.py b/src/smyth/server/app.py index d0de465..253bcee 100644 --- a/src/smyth/server/app.py +++ b/src/smyth/server/app.py @@ -1,6 +1,8 @@ import logging +from collections.abc import AsyncGenerator from contextlib import asynccontextmanager from multiprocessing import set_start_method +from typing import Any from starlette.applications import Starlette @@ -18,7 +20,7 @@ @asynccontextmanager -async def lifespan(app: "SmythStarlette"): +async def lifespan(app: "SmythStarlette") -> AsyncGenerator[None, None]: try: app.smyth.start_runners() except Exception as error: @@ -31,7 +33,7 @@ async def lifespan(app: "SmythStarlette"): class SmythStarlette(Starlette): smyth: Smyth - def __init__(self, smyth: Smyth, smyth_path_prefix: str, *args, **kwargs): + def __init__(self, smyth: Smyth, smyth_path_prefix: str, *args: Any, **kwargs: Any): self.smyth = smyth kwargs["lifespan"] = lifespan super().__init__(*args, **kwargs) @@ -50,7 +52,7 @@ def __init__(self, smyth: Smyth, smyth_path_prefix: str, *args, **kwargs): ) -def create_app(): +def create_app() -> SmythStarlette: LOGGER.debug("Creating app") config = get_config(get_config_dict()) @@ -71,6 +73,7 @@ def create_app(): log_level=handler_config.log_level, concurrency=handler_config.concurrency, strategy_generator=import_attribute(handler_config.strategy_generator_path), + env_overrides=handler_config.get_env_overrides(config), ) app = SmythStarlette(smyth=smyth, smyth_path_prefix=config.smyth_path_prefix) diff --git a/src/smyth/server/endpoints.py b/src/smyth/server/endpoints.py index 680fed8..3760d76 100644 --- a/src/smyth/server/endpoints.py +++ b/src/smyth/server/endpoints.py @@ -18,7 +18,7 @@ async def dispatch( smyth_handler: SmythHandler, request: Request, event_data_function: EventDataCallable | None = None, -): +) -> Response: """ Dispatches a request to Smyth and translates a Smyth response to a Starlette response. @@ -40,19 +40,19 @@ async def dispatch( ) return Response( - content=result.get("body", result), - status_code=result.get("statusCode", 200), - headers=result.get("headers", {}), + content=result.body, + status_code=result.status_code, + headers=result.headers, ) -async def lambda_invoker_endpoint(request: Request): +async def lambda_invoker_endpoint(request: Request) -> Response: smyth: Smyth = request.app.smyth smyth_handler = smyth.get_handler_for_request(request.url.path) return await dispatch(smyth, smyth_handler, request) -async def invocation_endpoint(request: Request): +async def invocation_endpoint(request: Request) -> Response: smyth: Smyth = request.app.smyth function = request.path_params["function"] try: @@ -70,7 +70,7 @@ async def invocation_endpoint(request: Request): ) -async def status_endpoint(request: Request): +async def status_endpoint(request: Request) -> Response: smyth: Smyth = request.app.smyth response_data: dict[str, Any] = { @@ -78,11 +78,11 @@ async def status_endpoint(request: Request): } for process_group_name, process_group in smyth.processes.items(): - response_data["lambda handlers"][process_group_name] = { # type: ignore[index] + response_data["lambda handlers"][process_group_name] = { "processes": [], } for process in process_group: - response_data["lambda handlers"][process_group_name]["processes"].append( # type: ignore[index] + response_data["lambda handlers"][process_group_name]["processes"].append( { "state": process.state, "task_counter": process.task_counter, diff --git a/src/smyth/smyth.py b/src/smyth/smyth.py index f3a6c7d..a0c3b73 100644 --- a/src/smyth/smyth.py +++ b/src/smyth/smyth.py @@ -1,6 +1,8 @@ import logging import logging.config from collections.abc import Iterator +from types import TracebackType +from typing import TypeVar from starlette.requests import Request from starlette.routing import compile_path @@ -12,12 +14,18 @@ from smyth.runner.strategy import first_warm from smyth.types import ( ContextDataCallable, + Environ, + EventData, EventDataCallable, + LambdaResponse, + RunnerInputMessage, RunnerProcessProtocol, SmythHandler, StrategyGenerator, ) +Self = TypeVar("Self", bound="Smyth") + LOGGER = logging.getLogger(__name__) @@ -42,7 +50,8 @@ def add_handler( log_level: str = "INFO", concurrency: int = 1, strategy_generator: StrategyGenerator = first_warm, - ): + env_overrides: Environ | None = None, + ) -> None: self.smyth_handlers[name] = SmythHandler( name=name, url_path=compile_path(path)[0], @@ -53,16 +62,22 @@ def add_handler( log_level=log_level, concurrency=concurrency, strategy_generator=strategy_generator, + env_overrides=env_overrides, ) - def __enter__(self): + def __enter__(self: Self) -> Self: self.start_runners() return self - def __exit__(self, exc_type, exc_value, traceback): + def __exit__( + self, + exc_type: type[BaseException], + exc_value: BaseException, + traceback: TracebackType, + ) -> None: self.stop_runners() - def start_runners(self): + def start_runners(self) -> None: for handler_name, handler_config in self.smyth_handlers.items(): self.processes[handler_name] = [] for index in range(handler_config.concurrency): @@ -70,6 +85,7 @@ def start_runners(self): name=f"{handler_name}:{index}", lambda_handler_path=handler_config.lambda_handler_path, log_level=handler_config.log_level, + environ_override=handler_config.get_environ(), ) process.start() LOGGER.info("Started process %s", process.name) @@ -78,7 +94,7 @@ def start_runners(self): handler_name, self.processes ) - def stop_runners(self): + def stop_runners(self) -> None: for process_group in self.processes.values(): for process in process_group: LOGGER.info("Stopping process %s", process.name) @@ -106,14 +122,15 @@ async def dispatch( smyth_handler: SmythHandler, request: Request, event_data_function: EventDataCallable | None = None, - ): + ) -> LambdaResponse | None: """ Smyth.dispatch is used upon a request that would normally be formed by an AWS trigger. It is responsible for finding the appropriate process for the request, invoking the process, and translating the response """ - process = next(self.strategy_generators[smyth_handler.name]) - if process is None: + try: + process = next(self.strategy_generators[smyth_handler.name]) + except KeyError: raise ProcessDefinitionNotFoundError( f"No process definition found for handler {smyth_handler.name}" ) @@ -121,36 +138,39 @@ async def dispatch( if event_data_function is None: event_data_function = smyth_handler.event_data_function - event_data = await event_data_function(request) + event_data = await event_data_function(request, smyth_handler, process) context_data = await smyth_handler.context_data_function( request, smyth_handler, process ) return await process.asend( - { - "type": "smyth.lambda.invoke", - "event": event_data, - "context": context_data, - } + RunnerInputMessage( + type="smyth.lambda.invoke", + event=event_data, + context=context_data, + ) ) - async def invoke(self, handler: SmythHandler, event_data: dict): + async def invoke( + self, handler: SmythHandler, event_data: EventData + ) -> LambdaResponse | None: """ Smyth.invoke is used to invoke a handler directly, without going through Starlette or when a direct invocation is needed (e.g., when invoking a lambda with boto3) - on direct invocation the event holds only the data passed in the invokation. There's no Starlette request involved. """ - process = next(self.strategy_generators[handler.name]) - if process is None: + try: + process = next(self.strategy_generators[handler.name]) + except KeyError: raise ProcessDefinitionNotFoundError( f"No process definition found for handler {handler.name}" ) context_data = await handler.context_data_function(None, handler, process) return await process.asend( - { - "type": "smyth.lambda.invoke", - "event": event_data, - "context": context_data, - } + RunnerInputMessage( + type="smyth.lambda.invoke", + event=event_data, + context=context_data, + ) ) diff --git a/src/smyth/types.py b/src/smyth/types.py index 1b8f896..c21e728 100644 --- a/src/smyth/types.py +++ b/src/smyth/types.py @@ -1,23 +1,30 @@ +import os +import sys from collections.abc import Awaitable, Callable, Iterator, MutableMapping from dataclasses import dataclass from enum import Enum from re import Pattern -from typing import Any, Protocol +from time import strftime +from typing import Annotated, Any, Literal, Protocol, TypeAlias from aws_lambda_powertools.utilities.typing import LambdaContext +from pydantic import BaseModel, Field from starlette.requests import Request -LambdaEvent = MutableMapping[str, Any] -LambdaHandler = Callable[[LambdaEvent, LambdaContext], dict[str, Any]] -RunnerMessage = MutableMapping[str, Any] -EventDataCallable = Callable[[Request], Awaitable[dict[str, Any]]] -ContextDataCallable = Callable[ - [Request | None, "SmythHandler", "RunnerProcessProtocol"], Awaitable[dict[str, Any]] +LambdaEvent: TypeAlias = MutableMapping[str, Any] +EventData: TypeAlias = dict[str, Any] +EventDataCallable: TypeAlias = Callable[ + [Request, "SmythHandler", "RunnerProcessProtocol"], Awaitable[EventData] ] -StrategyGenerator = Callable[ +ContextData: TypeAlias = dict[str, Any] +ContextDataCallable: TypeAlias = Callable[ + [Request | None, "SmythHandler", "RunnerProcessProtocol"], Awaitable[ContextData] +] +StrategyGenerator: TypeAlias = Callable[ [str, dict[str, list["RunnerProcessProtocol"]]], Iterator["RunnerProcessProtocol"], ] +Environ: TypeAlias = dict[str, str] class SmythHandlerState(str, Enum): @@ -26,23 +33,67 @@ class SmythHandlerState(str, Enum): WARM = "warm" +class RunnerInputMessage(BaseModel): + type: str + event: EventData | None = None + context: ContextData | None = None + + +class LambdaResponse(BaseModel): + status_code: int = Field(200, alias="statusCode") + headers: dict[str, str] = {} + body: str + + +class LambdaErrorResponse(BaseModel): + type: str + message: str + stacktrace: str + + +class RunnerStatusMessage(BaseModel): + type: Literal["smyth.lambda.status"] + status: SmythHandlerState + + +class RunnerResponseMessage(BaseModel): + type: Literal["smyth.lambda.response"] + response: LambdaResponse + + +class RunnerErrorMessage(BaseModel): + type: Literal["smyth.lambda.error"] + error: LambdaErrorResponse + + +RunnerOutputMessage = Annotated[ + RunnerStatusMessage | RunnerResponseMessage | RunnerErrorMessage, + Field(discriminator="type"), +] + + +LambdaHandler: TypeAlias = Callable[[LambdaEvent, LambdaContext], LambdaResponse] + + class RunnerProcessProtocol(Protocol): name: str task_counter: int last_used_timestamp: float state: SmythHandlerState - async def asend(self, data) -> RunnerMessage | None: ... + async def asend(self, data: RunnerInputMessage) -> LambdaResponse | None: ... + + def start(self) -> None: ... - def stop(self): ... + def stop(self) -> None: ... - def send(self, data) -> RunnerMessage | None: ... + def send(self, data: RunnerInputMessage) -> LambdaResponse | None: ... def is_alive(self) -> bool: ... - def terminate(self): ... + def terminate(self) -> None: ... - def join(self): ... + def join(self) -> None: ... @dataclass @@ -56,3 +107,66 @@ class SmythHandler: timeout: float | None = None log_level: str = "INFO" concurrency: int = 1 + env_overrides: Environ | None = None + + def _get_env_value(self, key: str, default: str) -> str: + """ + Helper method to retrieve an environment variable value with the following + precedence: + 1. `self.env_overrides` if defined. + 2. `os.environ` if the key exists. + 3. The provided default value. + """ + if self.env_overrides and key in self.env_overrides: + return self.env_overrides[key] + return os.environ.get(key, default) + + def get_environ(self) -> Environ: + envs = { + "_HANDLER": self._get_env_value("_HANDLER", self.lambda_handler_path), + "AWS_ACCESS_KEY_ID": self._get_env_value( + "AWS_ACCESS_KEY_ID", "000000000000" + ), + "AWS_SECRET_ACCESS_KEY": self._get_env_value( + "AWS_SECRET_ACCESS_KEY", "test" + ), + "AWS_SESSION_TOKEN": self._get_env_value("AWS_SESSION_TOKEN", "test"), + "AWS_DEFAULT_REGION": self._get_env_value( + "AWS_DEFAULT_REGION", "eu-central-1" + ), + "AWS_REGION": self._get_env_value("AWS_REGION", "eu-central-1"), + "AWS_EXECUTION_ENV": self._get_env_value( + "AWS_EXECUTION_ENV", + f"AWS_Lambda_python{sys.version_info.major}.{sys.version_info.minor}", + ), + "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": self._get_env_value( + "AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "128" + ), + "AWS_LAMBDA_FUNCTION_NAME": self._get_env_value( + "AWS_LAMBDA_FUNCTION_NAME", self.name + ), + "AWS_LAMBDA_FUNCTION_VERSION": self._get_env_value( + "AWS_LAMBDA_FUNCTION_VERSION", "$LATEST" + ), + "AWS_LAMBDA_INITIALIZATION_TYPE": self._get_env_value( + "AWS_LAMBDA_INITIALIZATION_TYPE", "on-demand" + ), + "AWS_LAMBDA_LOG_GROUP_NAME": self._get_env_value( + "AWS_LAMBDA_LOG_GROUP_NAME", f"/aws/lambda/{self.name}" + ), + "AWS_LAMBDA_LOG_STREAM_NAME": self._get_env_value( + "AWS_LAMBDA_LOG_STREAM_NAME", + f"{strftime('%Y/%m/%d')}/[$LATEST]smyth_aws_lambda_log_stream_name", + ), + "AWS_LAMBDA_RUNTIME_API": self._get_env_value( + "AWS_LAMBDA_RUNTIME_API", "127.0.0.1:9001" + ), + "AWS_XRAY_CONTEXT_MISSING": self._get_env_value( + "AWS_XRAY_CONTEXT_MISSING", "LOG_ERROR" + ), + "AWS_XRAY_DAEMON_ADDRESS": self._get_env_value( + "AWS_XRAY_DAEMON_ADDRESS", "127.0.0.1:2000" + ), + } + + return envs diff --git a/src/smyth/utils.py b/src/smyth/utils.py index e10f257..6bc47e2 100644 --- a/src/smyth/utils.py +++ b/src/smyth/utils.py @@ -4,6 +4,7 @@ from importlib import import_module from logging import LogRecord from pathlib import Path +from typing import Any from rich.console import Console, ConsoleRenderable, RenderableType from rich.logging import RichHandler @@ -14,8 +15,10 @@ FormatTimeCallable = Callable[[datetime], Text] -def get_logging_config(log_level: str, filter_path_prefix: str | None = None) -> dict: - logging_config = { +def get_logging_config( + log_level: str, filter_path_prefix: str | None = None +) -> dict[str, Any]: + logging_config: dict[str, Any] = { "version": 1, "disable_existing_loggers": False, "filters": {}, @@ -42,11 +45,11 @@ def get_logging_config(log_level: str, filter_path_prefix: str | None = None) -> }, } if filter_path_prefix: - logging_config["filters"]["smyth_api_filter"] = { # type: ignore[index] + logging_config["filters"]["smyth_api_filter"] = { "()": "smyth.utils.SmythStatusRouteFilter", "smyth_path_prefix": filter_path_prefix, } - logging_config["handlers"]["console"]["filters"].append("smyth_api_filter") # type: ignore[index] + logging_config["handlers"]["console"]["filters"].append("smyth_api_filter") return logging_config @@ -55,7 +58,7 @@ def __init__(self, name: str = "", smyth_path_prefix: str = "") -> None: super().__init__(name) self.smyth_path_prefix = smyth_path_prefix - def filter(self, record): + def filter(self, record: LogRecord) -> bool: return record.getMessage().find(self.smyth_path_prefix) == -1 @@ -99,7 +102,12 @@ def create_header_row(self, record: LogRecord) -> RenderableType: style="log.process", ) - def create_time_row(self, log_time, console, time_format) -> RenderableType | None: + def create_time_row( + self, + log_time: datetime | None, + console: Console, + time_format: str | FormatTimeCallable | None, + ) -> RenderableType | None: log_time = log_time or console.get_datetime() time_format = time_format or self.time_format if callable(time_format): @@ -183,7 +191,7 @@ def __call__( class SmythRichHandler(RichHandler): - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) self.rich_render = LogRender( show_time=True, @@ -222,7 +230,7 @@ def render( return log_renderable -def import_attribute(python_path: str): +def import_attribute(python_path: str) -> Any: module_name, handler_name = python_path.rsplit(".", 1) module = import_module(module_name) return getattr(module, handler_name) diff --git a/tests/conftest.py b/tests/conftest.py index 18264d2..7bea934 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ import pytest -from smyth.config import Config, HandlerConfig +from smyth.config import Config from smyth.types import RunnerProcessProtocol, SmythHandler, SmythHandlerState @@ -26,26 +26,40 @@ def smyth_handler( event_data_function=mock_event_data_function, context_data_function=mock_context_data_function, strategy_generator=mock_strategy_generator, + env_overrides={ + "TEST_ENV": "test", + }, ) @pytest.fixture -def config(): - return Config( - host="0.0.0.0", - port=8080, - handlers={ - "order_handler": HandlerConfig( - handler_path="tests.conftest.example_handler", - url_path=r"/test_handler", - ), - "product_handler": HandlerConfig( - handler_path="tests.conftest.example_handler", - url_path=r"/products/{path:path}", - ), - }, - log_level="INFO", - ) +def config_toml_dict(): + return { + "tool": { + "smyth": { + "host": "0.0.0.0", + "port": 8080, + "env": {"ROOT_ENV": "root", "TEST_ENV": "root"}, + "handlers": { + "order_handler": { + "handler_path": "tests.conftest.example_handler", + "url_path": "/test_handler", + "env": {"TEST_ENV": "child"}, + }, + "product_handler": { + "handler_path": "tests.conftest.example_handler", + "url_path": "/products/{path:path}", + }, + }, + "log_level": "INFO", + } + } + } + + +@pytest.fixture +def config(config_toml_dict): + return Config.from_dict(config_toml_dict["tool"]["smyth"]) def example_handler(event, context): @@ -59,12 +73,12 @@ def mock_lambda_handler(): @pytest.fixture def mock_event_data_function(): - return AsyncMock() + return AsyncMock(return_value={"key": "value"}) @pytest.fixture def mock_context_data_function(): - return AsyncMock() + return AsyncMock(return_value={"key": "value"}) @pytest.fixture diff --git a/tests/runner/test_fake_context.py b/tests/runner/test_fake_context.py index 849c4d1..e83c83b 100644 --- a/tests/runner/test_fake_context.py +++ b/tests/runner/test_fake_context.py @@ -1,20 +1,29 @@ -from time import strftime - import pytest +from freezegun.api import FrozenDateTimeFactory +from pytest_mock import MockerFixture from smyth.runner.fake_context import FakeLambdaContext -def test_fake_lambda_context(): +def test_fake_lambda_context(freezer: FrozenDateTimeFactory, mocker: MockerFixture): + freezer.move_to("2024-12-20 00:00:00") + expected_name = "Test Name Set In Test" + environ = mocker.patch.dict("os.environ", clear=True) + environ.update( + { + "AWS_LAMBDA_FUNCTION_NAME": expected_name, + } + ) context = FakeLambdaContext() - assert context.function_name == "Fake" - assert context.function_version == "LATEST" - assert context.invoked_function_arn == "arn:aws:lambda:serverless:Fake" - assert context.memory_limit_in_mb == "1024" + + assert context.function_name == expected_name + assert context.function_version == "$LATEST" + assert context.invoked_function_arn == f"arn:aws:lambda:serverless:{expected_name}" + assert context.memory_limit_in_mb == "128" assert context.aws_request_id == "1234567890" - assert context.log_group_name == "/aws/lambda/Fake" + assert context.log_group_name == f"/aws/lambda/{expected_name}" assert context.log_stream_name == ( - f"{strftime('%Y/%m/%d')}/[$LATEST]58419525dade4d17a495dceeeed44708" + "2024/12/20/[$LATEST]smyth_aws_lambda_log_stream_name" ) @@ -30,22 +39,29 @@ def test_fake_lambda_context(): [ ("test", "test", 60, "test", "test", 60), ("test", "test", None, "test", "test", 6), - ("test", None, 120, "test", "LATEST", 120), + ("test", None, 120, "test", "$LATEST", 120), (None, "test", 6, "Fake", "test", 6), - (None, None, 6, "Fake", "LATEST", 6), + (None, None, 6, "Fake", "$LATEST", 6), ], ) def test_fake_lambda_context_with_params( - name, version, timeout, expected_name, expected_version, expected_timeout + name, + version, + timeout, + expected_name, + expected_version, + expected_timeout, + freezer: FrozenDateTimeFactory, ): + freezer.move_to("2024-12-20 00:00:00") context = FakeLambdaContext(name=name, version=version, timeout=timeout) assert context.function_name == expected_name assert context.function_version == expected_version - assert context.timeout == expected_timeout + assert context._timeout == expected_timeout assert context.invoked_function_arn == f"arn:aws:lambda:serverless:{expected_name}" - assert context.memory_limit_in_mb == "1024" + assert context.memory_limit_in_mb == "128" assert context.aws_request_id == "1234567890" assert context.log_group_name == f"/aws/lambda/{expected_name}" assert context.log_stream_name == ( - f"{strftime('%Y/%m/%d')}/[${expected_version}]58419525dade4d17a495dceeeed44708" + f"2024/12/20/[{expected_version}]smyth_aws_lambda_log_stream_name" ) diff --git a/tests/runner/test_process.py b/tests/runner/test_process.py index 5cdec56..9d83812 100644 --- a/tests/runner/test_process.py +++ b/tests/runner/test_process.py @@ -5,7 +5,10 @@ from smyth.exceptions import LambdaHandlerLoadError, LambdaTimeoutError from smyth.runner.fake_context import FakeLambdaContext from smyth.runner.process import RunnerProcess -from smyth.types import SmythHandlerState +from smyth.types import ( + RunnerInputMessage, + SmythHandlerState, +) pytestmark = pytest.mark.anyio @@ -64,35 +67,40 @@ def test_run(mocker, mock_setproctitle, mock_logging_dictconfig, runner_process) def test_get_message(mocker, runner_process): mock_input_queue = mocker.patch.object(runner_process, "input_queue", autospec=True) mock_input_queue.get.side_effect = [ - {"type": "smyth.lambda.invoke", "event": {}, "context": {}}, + RunnerInputMessage(type="smyth.lambda.invoke", event={}, context={}), Empty, - {"type": "smyth.lambda.invoke", "event": {}, "context": {}}, - {"type": "smyth.stop"}, + RunnerInputMessage(type="smyth.lambda.invoke", event={}, context={}), + RunnerInputMessage(type="smyth.stop"), ] messages = list(runner_process.get_message__()) assert len(messages) == 2 - assert messages[0]["type"] == "smyth.lambda.invoke" - assert messages[1]["type"] == "smyth.lambda.invoke" + assert messages[0].type == "smyth.lambda.invoke" + assert messages[1].type == "smyth.lambda.invoke" def test_get_event(mocker, runner_process): assert ( - runner_process.get_event__({"type": "smyth.lambda.invoke", "event": {}}) == {} + runner_process.get_event__( + RunnerInputMessage(type="smyth.lambda.invoke", event={}, context={}) + ) + == {} ) def test_get_context(mocker, runner_process): assert isinstance( - runner_process.get_context__({"type": "smyth.lambda.invoke", "context": {}}), + runner_process.get_context__( + RunnerInputMessage(type="smyth.lambda.invoke", event={}, context={}) + ), FakeLambdaContext, ) assert ( runner_process.get_context__( - {"type": "smyth.lambda.invoke", "context": {}} - ).timeout + RunnerInputMessage(type="smyth.lambda.invoke", event={}, context={}) + )._timeout == 6 ) @@ -130,6 +138,7 @@ def test_timeout_handler(runner_process): def test_lambda_invoker(mocker, runner_process): mock_handler = mocker.Mock() + mock_handler.return_value = {"statusCode": 200, "body": "Hello, World!"} mock_import_attribute = mocker.patch( "smyth.runner.process.import_attribute", autospec=True, @@ -145,8 +154,12 @@ def test_lambda_invoker(mocker, runner_process): "get_message__", autospec=True, return_value=[ - {"type": "smyth.lambda.invoke", "event": {"test": "1"}, "context": {}}, - {"type": "smyth.lambda.invoke", "event": {"test": "2"}, "context": {}}, + RunnerInputMessage( + type="smyth.lambda.invoke", event={"test": "1"}, context={} + ), + RunnerInputMessage( + type="smyth.lambda.invoke", event={"test": "2"}, context={} + ), ], ) mocker.patch.object( diff --git a/tests/server/test_app.py b/tests/server/test_app.py index 8e084e4..ae7e757 100644 --- a/tests/server/test_app.py +++ b/tests/server/test_app.py @@ -38,6 +38,7 @@ def test_create_app(mocker, mock_get_config): log_level="DEBUG", concurrency=1, strategy_generator=first_warm, + env_overrides={"TEST_ENV": "child", "ROOT_ENV": "root"}, ), mocker.call( name="product_handler", @@ -49,6 +50,7 @@ def test_create_app(mocker, mock_get_config): log_level="DEBUG", concurrency=1, strategy_generator=first_warm, + env_overrides={"TEST_ENV": "root", "ROOT_ENV": "root"}, ), ] ) diff --git a/tests/server/test_endpoints.py b/tests/server/test_endpoints.py index c4cb2fd..b32b3ee 100644 --- a/tests/server/test_endpoints.py +++ b/tests/server/test_endpoints.py @@ -4,6 +4,7 @@ from smyth.exceptions import LambdaInvocationError, LambdaTimeoutError, SubprocessError from smyth.server.app import SmythStarlette from smyth.server.endpoints import dispatch +from smyth.types import LambdaResponse pytestmark = pytest.mark.anyio @@ -62,11 +63,11 @@ async def test_dispatch( ): mock_request = mocker.Mock() mock_event_data_function = mocker.Mock() - mock_smyth_dispatch.return_value = { - "body": "Hello, World!", - "statusCode": 200, - "headers": {}, - } + mock_smyth_dispatch.return_value = LambdaResponse( + body="Hello, World!", + status_code=200, + headers={}, + ) mock_smyth_dispatch.side_effect = side_effect response = await dispatch( smyth=mock_smyth, @@ -108,11 +109,11 @@ def test_status_endpoint(test_client): def test_invocation_endpoint(test_client, mock_smyth, mock_smyth_dispatch): - mock_smyth_dispatch.return_value = { - "body": "Hello, World!", - "statusCode": 200, - "headers": {}, - } + mock_smyth_dispatch.return_value = LambdaResponse( + body="Hello, World!", + status_code=200, + headers={}, + ) response = test_client.post( "/2015-03-31/functions/order_handler/invocations", json={"test": "test"}, diff --git a/tests/test_config.py b/tests/test_config.py index 4c5e6fb..71ae06c 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -6,7 +6,6 @@ import pytest from smyth.config import ( - Config, HandlerConfig, get_config, get_config_dict, @@ -42,28 +41,8 @@ def test_get_config_dict(mocker): mock_get_config_file_path.assert_called_once_with("other.toml") -def test_get_config(): - config_dict = { - "tool": { - "smyth": { - "host": "0.0.0.0", - "port": 8080, - "handlers": { - "order_handler": { - "handler_path": "tests.conftest.example_handler", - "url_path": "/test_handler", - }, - "product_handler": { - "handler_path": "tests.conftest.example_handler", - "url_path": "/products/{path:path}", - }, - }, - "log_level": "INFO", - } - } - } - - config = get_config(config_dict) +def test_get_config(config_toml_dict): + config = get_config(config_toml_dict) assert config.host == "0.0.0.0" assert config.port == 8080 @@ -71,6 +50,7 @@ def test_get_config(): "order_handler": HandlerConfig( handler_path="tests.conftest.example_handler", url_path=r"/test_handler", + env={"TEST_ENV": "child"}, ), "product_handler": HandlerConfig( handler_path="tests.conftest.example_handler", @@ -84,21 +64,18 @@ def test_get_config(): assert get_config(None) == config -def test_serialize_config(): - config = Config( - host="0.0.0.0", - port=8080, - handlers={ - "order_handler": HandlerConfig( - handler_path="tests.conftest.example_handler", - url_path=r"/test_handler", - ), - "product_handler": HandlerConfig( - handler_path="tests.conftest.example_handler", - url_path=r"/products/{path:path}", - ), - }, - log_level="INFO", +def test_serialize_config(config): + assert serialize_config(config) == json.dumps(asdict(config)) + + +def test_get_env_overrides(config): + handler_config = HandlerConfig( + handler_path="tests.conftest.example_handler", + url_path=r"/test_handler", + env={"TEST_ENV": "test"}, ) - assert serialize_config(config) == json.dumps(asdict(config)) + assert handler_config.get_env_overrides(config) == { + "ROOT_ENV": "root", + "TEST_ENV": "test", + } diff --git a/tests/test_context.py b/tests/test_context.py index 81b8e77..ed746d6 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -22,6 +22,7 @@ async def test_generate_context_data( "strategy_generator": ANY, "timeout": None, "url_path": re.compile("/test_handler"), + "env_overrides": {"TEST_ENV": "test"}, }, "name": "test_handler", }, diff --git a/tests/test_event.py b/tests/test_event.py index 0e2c33a..88ae18f 100644 --- a/tests/test_event.py +++ b/tests/test_event.py @@ -19,7 +19,9 @@ async def test_generate_api_gw_v2_event_data(mocker): mock_request.url.query = "" mock_request.url.scheme = "http" - assert await generate_api_gw_v2_event_data(mock_request) == { + assert await generate_api_gw_v2_event_data( + mock_request, mocker.Mock(), mocker.Mock() + ) == { "version": "2.0", "rawPath": "/test", "body": "", @@ -46,4 +48,6 @@ async def test_generate_lambda_invokation_event_data(mocker): mock_request = mocker.Mock() mock_request.json = mocker.AsyncMock(return_value={"test": "test"}) - assert await generate_lambda_invocation_event_data(mock_request) == {"test": "test"} + assert await generate_lambda_invocation_event_data( + mock_request, mocker.Mock(), mocker.Mock() + ) == {"test": "test"} diff --git a/tests/test_smyth.py b/tests/test_smyth.py index 670e930..7cad39e 100644 --- a/tests/test_smyth.py +++ b/tests/test_smyth.py @@ -77,9 +77,9 @@ async def test_smyth_dispatch( mock_request, ) - assert mock_asend.await_args[0][0]["type"] == "smyth.lambda.invoke" - assert mock_asend.await_args[0][0]["event"] == await mock_event_data_function() - assert mock_asend.await_args[0][0]["context"] == await mock_context_data_function() + assert mock_asend.await_args[0][0].type == "smyth.lambda.invoke" + assert mock_asend.await_args[0][0].event == await mock_event_data_function() + assert mock_asend.await_args[0][0].context == await mock_context_data_function() assert response == mock_asend.return_value @@ -95,7 +95,7 @@ async def test_invoke( event_data, ) - assert mock_asend.await_args[0][0]["type"] == "smyth.lambda.invoke" - assert mock_asend.await_args[0][0]["event"] == event_data - assert mock_asend.await_args[0][0]["context"] == await mock_context_data_function() + assert mock_asend.await_args[0][0].type == "smyth.lambda.invoke" + assert mock_asend.await_args[0][0].event == event_data + assert mock_asend.await_args[0][0].context == await mock_context_data_function() assert response == mock_asend.return_value