diff --git a/.fernignore b/.fernignore new file mode 100644 index 0000000..084a8eb --- /dev/null +++ b/.fernignore @@ -0,0 +1 @@ +# Specify files that shouldn't be modified by Fern diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..63cfe46 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,60 @@ +name: ci + +on: [push] +jobs: + compile: + runs-on: ubuntu-20.04 + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Bootstrap poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install + - name: Compile + run: poetry run mypy . + test: + runs-on: ubuntu-20.04 + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Bootstrap poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install + - name: Test + run: poetry run pytest ./tests/custom/ + + publish: + needs: [compile, test] + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-20.04 + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + - name: Bootstrap poetry + run: | + curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + - name: Install dependencies + run: poetry install + - name: Publish to pypi + run: | + poetry config repositories.remote https://upload.pypi.org/legacy/ + poetry --no-interaction -v publish --build --repository remote --username "$PYPI_USERNAME" --password "$PYPI_PASSWORD" + env: + PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }} + PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} diff --git a/.gitignore b/.gitignore index 68bc17f..42cb863 100644 --- a/.gitignore +++ b/.gitignore @@ -1,160 +1,4 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ dist/ -downloads/ -eggs/ -.eggs/ -lib/ -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 - -# 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 - -# 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/#use-with-ide -.pdm.toml - -# 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 -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# 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/ +__pycache__/ +poetry.toml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..05f0fb0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Fileforge. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index b149216..9f1837f 100644 --- a/README.md +++ b/README.md @@ -1 +1,64 @@ -# fileforge-python-sdk \ No newline at end of file + +# Fileforge Python Library + +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-SDK%20generated%20by%20Fern-brightgreen)](https://github.com/fern-api/fern) + +The Fileforge Python Library provides convenient access to the Fileforge API from applications written in Python. + + + +# Installation + +```sh +pip install --upgrade fileforge +``` + + + +# Usage + +```python +from fileforge.client import Fileforge + +client = Fileforge( + api_key="YOUR_API_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", +) +``` + + + +# Async Client + +```python +from fileforge.client import AsyncFileforge + +client = AsyncFileforge( + api_key="YOUR_API_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", +) +``` + + + +# Beta Status + +This SDK is in beta, and there may be breaking changes between versions without a major +version update. Therefore, we recommend pinning the package version to a specific version. +This way, you can install the same version each time without breaking changes. + + + +# Contributing + +While we value open-source contributions to this SDK, this library is generated programmatically. +Additions made directly to this library would have to be moved over to our generation code, +otherwise they would be overwritten upon the next generated release. Feel free to open a PR as + a proof of concept, but know that we will not be able to merge it as-is. We suggest opening +an issue first to discuss with us! + +On the other hand, contributions to the README are always very welcome! + + diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..44d2caa --- /dev/null +++ b/poetry.lock @@ -0,0 +1,448 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.9\""} + +[[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] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[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 = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[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 = "exceptiongroup" +version = "1.2.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[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 = "httpcore" +version = "1.0.5" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.26.0)"] + +[[package]] +name = "httpx" +version = "0.27.0" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "mypy" +version = "1.9.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, + {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, + {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, + {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, + {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, + {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, + {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, + {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, + {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, + {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, + {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, + {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, + {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, + {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, + {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, + {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, + {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, + {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +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 = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pydantic" +version = "2.7.1" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"}, + {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.18.2" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.18.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"}, + {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"}, + {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"}, + {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"}, + {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"}, + {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"}, + {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"}, + {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"}, + {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"}, + {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"}, + {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"}, + {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"}, + {file = "pydantic_core-2.18.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439"}, + {file = "pydantic_core-2.18.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b"}, + {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761"}, + {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788"}, + {file = "pydantic_core-2.18.2-cp38-none-win32.whl", hash = "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350"}, + {file = "pydantic_core-2.18.2-cp38-none-win_amd64.whl", hash = "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e"}, + {file = "pydantic_core-2.18.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8"}, + {file = "pydantic_core-2.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4"}, + {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399"}, + {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b"}, + {file = "pydantic_core-2.18.2-cp39-none-win32.whl", hash = "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e"}, + {file = "pydantic_core-2.18.2-cp39-none-win_amd64.whl", hash = "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"}, + {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.23.7" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"}, + {file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[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.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"}, + {file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.8" +content-hash = "6f0b8d5044d8d4461b9850fcdfd7c382335fc9481fcfd5bd42e760121c1e1450" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..680a340 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,56 @@ +[tool.poetry] +name = "fileforge" +version = "0.0.13" +description = "" +readme = "README.md" +authors = [] +keywords = [] +license = "MIT" +classifiers = [ + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed", + "License :: OSI Approved :: MIT License" +] +packages = [ + { include = "fileforge", from = "src"} +] + +[project.urls] +Repository = 'https://github.com/OnedocLabs/fileforge-python-sdk' + +[tool.poetry.dependencies] +python = "^3.8" +httpx = ">=0.21.2" +pydantic = ">= 1.9.2" +typing_extensions = ">= 4.0.0" + +[tool.poetry.dev-dependencies] +mypy = "1.9.0" +pytest = "^7.4.0" +pytest-asyncio = "^0.23.5" +python-dateutil = "^2.9.0" + +[tool.pytest.ini_options] +testpaths = [ "tests" ] +asyncio_mode = "auto" + +[tool.mypy] +plugins = ["pydantic.mypy"] + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/src/fileforge/__init__.py b/src/fileforge/__init__.py new file mode 100644 index 0000000..b6d9b03 --- /dev/null +++ b/src/fileforge/__init__.py @@ -0,0 +1,19 @@ +# This file was auto-generated by Fern from our API Definition. + +from .types import ConvertDocxRequestOptions, ErrorSchema, GenerateRequestOptions, MergeRequestOptions +from .errors import BadGatewayError, BadRequestError, InternalServerError, UnauthorizedError +from .environment import FileforgeEnvironment +from .version import __version__ + +__all__ = [ + "BadGatewayError", + "BadRequestError", + "ConvertDocxRequestOptions", + "ErrorSchema", + "FileforgeEnvironment", + "GenerateRequestOptions", + "InternalServerError", + "MergeRequestOptions", + "UnauthorizedError", + "__version__", +] diff --git a/src/fileforge/client.py b/src/fileforge/client.py new file mode 100644 index 0000000..71123de --- /dev/null +++ b/src/fileforge/client.py @@ -0,0 +1,849 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +import urllib.parse +from json.decoder import JSONDecodeError + +import httpx + +from . import core +from .core.api_error import ApiError +from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .core.jsonable_encoder import jsonable_encoder +from .core.pydantic_utilities import pydantic_v1 +from .core.query_encoder import encode_query +from .core.remove_none_from_dict import remove_none_from_dict +from .core.request_options import RequestOptions +from .environment import FileforgeEnvironment +from .errors.bad_gateway_error import BadGatewayError +from .errors.bad_request_error import BadRequestError +from .errors.internal_server_error import InternalServerError +from .errors.unauthorized_error import UnauthorizedError +from .types.convert_docx_request_options import ConvertDocxRequestOptions +from .types.error_schema import ErrorSchema +from .types.generate_request_options import GenerateRequestOptions +from .types.merge_request_options import MergeRequestOptions + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class Fileforge: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : typing.Optional[str] + The base url to use for requests from the client. + + environment : FileforgeEnvironment + The environment to use for requests from the client. from .environment import FileforgeEnvironment + + + + Defaults to FileforgeEnvironment.DEFAULT + + + + api_key : str + username : typing.Optional[typing.Union[str, typing.Callable[[], str]]] + password : typing.Optional[typing.Union[str, typing.Callable[[], str]]] + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests by default the timeout is 60 seconds, unless a custom httpx client is used, in which case a default is not set. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.Client] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + Examples + -------- + from fileforge.client import Fileforge + + client = Fileforge( + api_key="YOUR_API_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + ) + """ + + def __init__( + self, + *, + base_url: typing.Optional[str] = None, + environment: FileforgeEnvironment = FileforgeEnvironment.DEFAULT, + api_key: str, + username: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + password: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.Client] = None, + ): + _defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None + self._client_wrapper = SyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + api_key=api_key, + username=username, + password=password, + httpx_client=httpx_client + if httpx_client is not None + else httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.Client(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ) + + def retrieve_server_status(self, *, request_options: typing.Optional[RequestOptions] = None) -> None: + """ + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + from fileforge.client import Fileforge + + client = Fileforge( + api_key="YOUR_API_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + ) + client.retrieve_server_status() + """ + _response = self._client_wrapper.httpx_client.request( + method="GET", + url=urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "status/"), + params=encode_query( + jsonable_encoder( + request_options.get("additional_query_parameters") if request_options is not None else None + ) + ), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self._client_wrapper.get_headers(), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ), + timeout=request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self._client_wrapper.get_timeout(), + retries=0, + max_retries=request_options.get("max_retries") if request_options is not None else 0, # type: ignore + ) + if 200 <= _response.status_code < 300: + return + try: + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + def convert_docx( + self, + *, + options: ConvertDocxRequestOptions, + file: core.File, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.Iterator[bytes]: + """ + Converts a Microsoft Word document (.DOCX or .DOC) file to a PDF document. + + This service uses a LibreOffice headless server to perform the conversion, and may not support all features of the original document. + + **Known discrepancies** + + - Some fonts may not be available in the server, and may be substituted by a closest match. + - Some complex formatting may not be preserved, such as background graphics. + + **Variables** + + Variable replacement is supported with various methods: + + - Templated litterals: `{{name}}` + - Word variables, as listed in the document metadata: `{DOCVARIABLE "name"}` + + To enable variable replacement as Word variables for your account, please contact the FileForge support. + + Parameters + ---------- + options : ConvertDocxRequestOptions + Conversion options. This field is required even if empty. + + **Options** + + * `templateLiterals`: Map of template literals to replace in the document. Template literals should be enclosed in double curly braces, e.g. `{{name}}`. Variables name can contain alphanumeric characters and hyphens. All variables are case-sensitive. The value for each variable should be a string. If a value of undefined is passed, the variable will not be removed from the document. If you need to remove a variable, pass an empty string as the value. + + **NB** variables should **not** have surrounding spaces, e.g. `{{ name }}`. + + **Example** + + In the Word document: `{{name}} {{nickname}}. was born on {{date}}.` + + ```json + { + "templateLiterals": { + "name": "John Doe", + "date": "2021-12-31", + "nickname": "" + } + } + ``` + + There will not be an error if a variable is not found in the document, nor if variables found in the document are not in the options. + + file : core.File + See core.File for more documentation + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Yields + ------ + typing.Iterator[bytes] + PDF Document generated successfully + + Examples + -------- + from fileforge.client import Fileforge + + client = Fileforge( + api_key="YOUR_API_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + ) + client.convert_docx() + """ + with self._client_wrapper.httpx_client.stream( + method="POST", + url=urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "pdf/docx/"), + params=encode_query( + jsonable_encoder( + request_options.get("additional_query_parameters") if request_options is not None else None + ) + ), + data=jsonable_encoder(remove_none_from_dict({"options": options})) + if request_options is None or request_options.get("additional_body_parameters") is None + else { + **jsonable_encoder(remove_none_from_dict({"options": options})), + **(jsonable_encoder(remove_none_from_dict(request_options.get("additional_body_parameters", {})))), + }, + files=core.convert_file_dict_to_httpx_tuples(remove_none_from_dict({"file": file})), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self._client_wrapper.get_headers(), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ), + timeout=request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self._client_wrapper.get_timeout(), + retries=0, + max_retries=request_options.get("max_retries") if request_options is not None else 0, # type: ignore + ) as _response: + if 200 <= _response.status_code < 300: + for _chunk in _response.iter_bytes(): + yield _chunk + return + _response.read() + if _response.status_code == 400: + raise BadRequestError(pydantic_v1.parse_obj_as(ErrorSchema, _response.json())) # type: ignore + if _response.status_code == 401: + raise UnauthorizedError(pydantic_v1.parse_obj_as(ErrorSchema, _response.json())) # type: ignore + if _response.status_code == 500: + raise InternalServerError(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore + try: + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + def generate( + self, + *, + options: GenerateRequestOptions, + files: typing.List[core.File], + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.Iterator[bytes]: + """ + Generates a PDF document from web assets. + + Parameters + ---------- + options : GenerateRequestOptions + Conversion options. This field is required even if empty. + + files : typing.List[core.File] + See core.File for more documentation + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Yields + ------ + typing.Iterator[bytes] + PDF Document generated successfully + + Examples + -------- + from fileforge.client import Fileforge + + client = Fileforge( + api_key="YOUR_API_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + ) + client.generate() + """ + with self._client_wrapper.httpx_client.stream( + method="POST", + url=urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "pdf/generate/"), + params=encode_query( + jsonable_encoder( + request_options.get("additional_query_parameters") if request_options is not None else None + ) + ), + data=jsonable_encoder(remove_none_from_dict({"options": options})) + if request_options is None or request_options.get("additional_body_parameters") is None + else { + **jsonable_encoder(remove_none_from_dict({"options": options})), + **(jsonable_encoder(remove_none_from_dict(request_options.get("additional_body_parameters", {})))), + }, + files=core.convert_file_dict_to_httpx_tuples(remove_none_from_dict({"files": files})), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self._client_wrapper.get_headers(), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ), + timeout=request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self._client_wrapper.get_timeout(), + retries=0, + max_retries=request_options.get("max_retries") if request_options is not None else 0, # type: ignore + ) as _response: + if 200 <= _response.status_code < 300: + for _chunk in _response.iter_bytes(): + yield _chunk + return + _response.read() + if _response.status_code == 400: + raise BadRequestError(pydantic_v1.parse_obj_as(ErrorSchema, _response.json())) # type: ignore + if _response.status_code == 401: + raise UnauthorizedError(pydantic_v1.parse_obj_as(ErrorSchema, _response.json())) # type: ignore + if _response.status_code == 500: + raise InternalServerError(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore + if _response.status_code == 502: + raise BadGatewayError(pydantic_v1.parse_obj_as(ErrorSchema, _response.json())) # type: ignore + try: + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + def merge( + self, + *, + options: MergeRequestOptions, + files: typing.List[core.File], + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.Iterator[bytes]: + """ + Parameters + ---------- + options : MergeRequestOptions + + files : typing.List[core.File] + See core.File for more documentation + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Yields + ------ + typing.Iterator[bytes] + PDF Document generated successfully + + Examples + -------- + from fileforge.client import Fileforge + + client = Fileforge( + api_key="YOUR_API_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + ) + client.merge() + """ + with self._client_wrapper.httpx_client.stream( + method="POST", + url=urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "pdf/merge/"), + params=encode_query( + jsonable_encoder( + request_options.get("additional_query_parameters") if request_options is not None else None + ) + ), + data=jsonable_encoder(remove_none_from_dict({"options": options})) + if request_options is None or request_options.get("additional_body_parameters") is None + else { + **jsonable_encoder(remove_none_from_dict({"options": options})), + **(jsonable_encoder(remove_none_from_dict(request_options.get("additional_body_parameters", {})))), + }, + files=core.convert_file_dict_to_httpx_tuples(remove_none_from_dict({"files": files})), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self._client_wrapper.get_headers(), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ), + timeout=request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self._client_wrapper.get_timeout(), + retries=0, + max_retries=request_options.get("max_retries") if request_options is not None else 0, # type: ignore + ) as _response: + if 200 <= _response.status_code < 300: + for _chunk in _response.iter_bytes(): + yield _chunk + return + _response.read() + if _response.status_code == 400: + raise BadRequestError(pydantic_v1.parse_obj_as(ErrorSchema, _response.json())) # type: ignore + if _response.status_code == 401: + raise UnauthorizedError(pydantic_v1.parse_obj_as(ErrorSchema, _response.json())) # type: ignore + if _response.status_code == 500: + raise InternalServerError(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore + try: + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + +class AsyncFileforge: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : typing.Optional[str] + The base url to use for requests from the client. + + environment : FileforgeEnvironment + The environment to use for requests from the client. from .environment import FileforgeEnvironment + + + + Defaults to FileforgeEnvironment.DEFAULT + + + + api_key : str + username : typing.Optional[typing.Union[str, typing.Callable[[], str]]] + password : typing.Optional[typing.Union[str, typing.Callable[[], str]]] + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests by default the timeout is 60 seconds, unless a custom httpx client is used, in which case a default is not set. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.AsyncClient] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + Examples + -------- + from fileforge.client import AsyncFileforge + + client = AsyncFileforge( + api_key="YOUR_API_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + ) + """ + + def __init__( + self, + *, + base_url: typing.Optional[str] = None, + environment: FileforgeEnvironment = FileforgeEnvironment.DEFAULT, + api_key: str, + username: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + password: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.AsyncClient] = None, + ): + _defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None + self._client_wrapper = AsyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + api_key=api_key, + username=username, + password=password, + httpx_client=httpx_client + if httpx_client is not None + else httpx.AsyncClient(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.AsyncClient(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ) + + async def retrieve_server_status(self, *, request_options: typing.Optional[RequestOptions] = None) -> None: + """ + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + None + + Examples + -------- + from fileforge.client import AsyncFileforge + + client = AsyncFileforge( + api_key="YOUR_API_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + ) + await client.retrieve_server_status() + """ + _response = await self._client_wrapper.httpx_client.request( + method="GET", + url=urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "status/"), + params=encode_query( + jsonable_encoder( + request_options.get("additional_query_parameters") if request_options is not None else None + ) + ), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self._client_wrapper.get_headers(), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ), + timeout=request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self._client_wrapper.get_timeout(), + retries=0, + max_retries=request_options.get("max_retries") if request_options is not None else 0, # type: ignore + ) + if 200 <= _response.status_code < 300: + return + try: + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + async def convert_docx( + self, + *, + options: ConvertDocxRequestOptions, + file: core.File, + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.AsyncIterator[bytes]: + """ + Converts a Microsoft Word document (.DOCX or .DOC) file to a PDF document. + + This service uses a LibreOffice headless server to perform the conversion, and may not support all features of the original document. + + **Known discrepancies** + + - Some fonts may not be available in the server, and may be substituted by a closest match. + - Some complex formatting may not be preserved, such as background graphics. + + **Variables** + + Variable replacement is supported with various methods: + + - Templated litterals: `{{name}}` + - Word variables, as listed in the document metadata: `{DOCVARIABLE "name"}` + + To enable variable replacement as Word variables for your account, please contact the FileForge support. + + Parameters + ---------- + options : ConvertDocxRequestOptions + Conversion options. This field is required even if empty. + + **Options** + + * `templateLiterals`: Map of template literals to replace in the document. Template literals should be enclosed in double curly braces, e.g. `{{name}}`. Variables name can contain alphanumeric characters and hyphens. All variables are case-sensitive. The value for each variable should be a string. If a value of undefined is passed, the variable will not be removed from the document. If you need to remove a variable, pass an empty string as the value. + + **NB** variables should **not** have surrounding spaces, e.g. `{{ name }}`. + + **Example** + + In the Word document: `{{name}} {{nickname}}. was born on {{date}}.` + + ```json + { + "templateLiterals": { + "name": "John Doe", + "date": "2021-12-31", + "nickname": "" + } + } + ``` + + There will not be an error if a variable is not found in the document, nor if variables found in the document are not in the options. + + file : core.File + See core.File for more documentation + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Yields + ------ + typing.AsyncIterator[bytes] + PDF Document generated successfully + + Examples + -------- + from fileforge.client import AsyncFileforge + + client = AsyncFileforge( + api_key="YOUR_API_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + ) + await client.convert_docx() + """ + async with self._client_wrapper.httpx_client.stream( + method="POST", + url=urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "pdf/docx/"), + params=encode_query( + jsonable_encoder( + request_options.get("additional_query_parameters") if request_options is not None else None + ) + ), + data=jsonable_encoder(remove_none_from_dict({"options": options})) + if request_options is None or request_options.get("additional_body_parameters") is None + else { + **jsonable_encoder(remove_none_from_dict({"options": options})), + **(jsonable_encoder(remove_none_from_dict(request_options.get("additional_body_parameters", {})))), + }, + files=core.convert_file_dict_to_httpx_tuples(remove_none_from_dict({"file": file})), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self._client_wrapper.get_headers(), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ), + timeout=request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self._client_wrapper.get_timeout(), + retries=0, + max_retries=request_options.get("max_retries") if request_options is not None else 0, # type: ignore + ) as _response: + if 200 <= _response.status_code < 300: + async for _chunk in _response.aiter_bytes(): + yield _chunk + return + await _response.aread() + if _response.status_code == 400: + raise BadRequestError(pydantic_v1.parse_obj_as(ErrorSchema, _response.json())) # type: ignore + if _response.status_code == 401: + raise UnauthorizedError(pydantic_v1.parse_obj_as(ErrorSchema, _response.json())) # type: ignore + if _response.status_code == 500: + raise InternalServerError(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore + try: + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + async def generate( + self, + *, + options: GenerateRequestOptions, + files: typing.List[core.File], + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.AsyncIterator[bytes]: + """ + Generates a PDF document from web assets. + + Parameters + ---------- + options : GenerateRequestOptions + Conversion options. This field is required even if empty. + + files : typing.List[core.File] + See core.File for more documentation + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Yields + ------ + typing.AsyncIterator[bytes] + PDF Document generated successfully + + Examples + -------- + from fileforge.client import AsyncFileforge + + client = AsyncFileforge( + api_key="YOUR_API_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + ) + await client.generate() + """ + async with self._client_wrapper.httpx_client.stream( + method="POST", + url=urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "pdf/generate/"), + params=encode_query( + jsonable_encoder( + request_options.get("additional_query_parameters") if request_options is not None else None + ) + ), + data=jsonable_encoder(remove_none_from_dict({"options": options})) + if request_options is None or request_options.get("additional_body_parameters") is None + else { + **jsonable_encoder(remove_none_from_dict({"options": options})), + **(jsonable_encoder(remove_none_from_dict(request_options.get("additional_body_parameters", {})))), + }, + files=core.convert_file_dict_to_httpx_tuples(remove_none_from_dict({"files": files})), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self._client_wrapper.get_headers(), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ), + timeout=request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self._client_wrapper.get_timeout(), + retries=0, + max_retries=request_options.get("max_retries") if request_options is not None else 0, # type: ignore + ) as _response: + if 200 <= _response.status_code < 300: + async for _chunk in _response.aiter_bytes(): + yield _chunk + return + await _response.aread() + if _response.status_code == 400: + raise BadRequestError(pydantic_v1.parse_obj_as(ErrorSchema, _response.json())) # type: ignore + if _response.status_code == 401: + raise UnauthorizedError(pydantic_v1.parse_obj_as(ErrorSchema, _response.json())) # type: ignore + if _response.status_code == 500: + raise InternalServerError(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore + if _response.status_code == 502: + raise BadGatewayError(pydantic_v1.parse_obj_as(ErrorSchema, _response.json())) # type: ignore + try: + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + async def merge( + self, + *, + options: MergeRequestOptions, + files: typing.List[core.File], + request_options: typing.Optional[RequestOptions] = None, + ) -> typing.AsyncIterator[bytes]: + """ + Parameters + ---------- + options : MergeRequestOptions + + files : typing.List[core.File] + See core.File for more documentation + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Yields + ------ + typing.AsyncIterator[bytes] + PDF Document generated successfully + + Examples + -------- + from fileforge.client import AsyncFileforge + + client = AsyncFileforge( + api_key="YOUR_API_KEY", + username="YOUR_USERNAME", + password="YOUR_PASSWORD", + ) + await client.merge() + """ + async with self._client_wrapper.httpx_client.stream( + method="POST", + url=urllib.parse.urljoin(f"{self._client_wrapper.get_base_url()}/", "pdf/merge/"), + params=encode_query( + jsonable_encoder( + request_options.get("additional_query_parameters") if request_options is not None else None + ) + ), + data=jsonable_encoder(remove_none_from_dict({"options": options})) + if request_options is None or request_options.get("additional_body_parameters") is None + else { + **jsonable_encoder(remove_none_from_dict({"options": options})), + **(jsonable_encoder(remove_none_from_dict(request_options.get("additional_body_parameters", {})))), + }, + files=core.convert_file_dict_to_httpx_tuples(remove_none_from_dict({"files": files})), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self._client_wrapper.get_headers(), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ), + timeout=request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self._client_wrapper.get_timeout(), + retries=0, + max_retries=request_options.get("max_retries") if request_options is not None else 0, # type: ignore + ) as _response: + if 200 <= _response.status_code < 300: + async for _chunk in _response.aiter_bytes(): + yield _chunk + return + await _response.aread() + if _response.status_code == 400: + raise BadRequestError(pydantic_v1.parse_obj_as(ErrorSchema, _response.json())) # type: ignore + if _response.status_code == 401: + raise UnauthorizedError(pydantic_v1.parse_obj_as(ErrorSchema, _response.json())) # type: ignore + if _response.status_code == 500: + raise InternalServerError(pydantic_v1.parse_obj_as(typing.Any, _response.json())) # type: ignore + try: + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, body=_response.text) + raise ApiError(status_code=_response.status_code, body=_response_json) + + +def _get_base_url(*, base_url: typing.Optional[str] = None, environment: FileforgeEnvironment) -> str: + if base_url is not None: + return base_url + elif environment is not None: + return environment.value + else: + raise Exception("Please pass in either base_url or environment to construct the client") diff --git a/src/fileforge/core/__init__.py b/src/fileforge/core/__init__.py new file mode 100644 index 0000000..58ad52a --- /dev/null +++ b/src/fileforge/core/__init__.py @@ -0,0 +1,30 @@ +# This file was auto-generated by Fern from our API Definition. + +from .api_error import ApiError +from .client_wrapper import AsyncClientWrapper, BaseClientWrapper, SyncClientWrapper +from .datetime_utils import serialize_datetime +from .file import File, convert_file_dict_to_httpx_tuples +from .http_client import AsyncHttpClient, HttpClient +from .jsonable_encoder import jsonable_encoder +from .pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 +from .query_encoder import encode_query +from .remove_none_from_dict import remove_none_from_dict +from .request_options import RequestOptions + +__all__ = [ + "ApiError", + "AsyncClientWrapper", + "AsyncHttpClient", + "BaseClientWrapper", + "File", + "HttpClient", + "RequestOptions", + "SyncClientWrapper", + "convert_file_dict_to_httpx_tuples", + "deep_union_pydantic_dicts", + "encode_query", + "jsonable_encoder", + "pydantic_v1", + "remove_none_from_dict", + "serialize_datetime", +] diff --git a/src/fileforge/core/api_error.py b/src/fileforge/core/api_error.py new file mode 100644 index 0000000..2e9fc54 --- /dev/null +++ b/src/fileforge/core/api_error.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + + +class ApiError(Exception): + status_code: typing.Optional[int] + body: typing.Any + + def __init__(self, *, status_code: typing.Optional[int] = None, body: typing.Any = None): + self.status_code = status_code + self.body = body + + def __str__(self) -> str: + return f"status_code: {self.status_code}, body: {self.body}" diff --git a/src/fileforge/core/client_wrapper.py b/src/fileforge/core/client_wrapper.py new file mode 100644 index 0000000..9289de3 --- /dev/null +++ b/src/fileforge/core/client_wrapper.py @@ -0,0 +1,85 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx + +from .http_client import AsyncHttpClient, HttpClient + + +class BaseClientWrapper: + def __init__( + self, + *, + api_key: str, + username: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + password: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + base_url: str, + timeout: typing.Optional[float] = None + ): + self._api_key = api_key + self._username = username + self._password = password + self._base_url = base_url + self._timeout = timeout + + def get_headers(self) -> typing.Dict[str, str]: + headers: typing.Dict[str, str] = { + "X-Fern-Language": "Python", + "X-Fern-SDK-Name": "fileforge", + "X-Fern-SDK-Version": "0.0.13", + } + username = self._get_username() + password = self._get_password() + if username is not None and password is not None: + headers["Authorization"] = httpx.BasicAuth(username, password)._auth_header + headers["X-API-Key"] = self._api_key + return headers + + def _get_username(self) -> typing.Optional[str]: + if isinstance(self._username, str) or self._username is None: + return self._username + else: + return self._username() + + def _get_password(self) -> typing.Optional[str]: + if isinstance(self._password, str) or self._password is None: + return self._password + else: + return self._password() + + def get_base_url(self) -> str: + return self._base_url + + def get_timeout(self) -> typing.Optional[float]: + return self._timeout + + +class SyncClientWrapper(BaseClientWrapper): + def __init__( + self, + *, + api_key: str, + username: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + password: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + httpx_client: httpx.Client + ): + super().__init__(api_key=api_key, username=username, password=password, base_url=base_url, timeout=timeout) + self.httpx_client = HttpClient(httpx_client=httpx_client) + + +class AsyncClientWrapper(BaseClientWrapper): + def __init__( + self, + *, + api_key: str, + username: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + password: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + httpx_client: httpx.AsyncClient + ): + super().__init__(api_key=api_key, username=username, password=password, base_url=base_url, timeout=timeout) + self.httpx_client = AsyncHttpClient(httpx_client=httpx_client) diff --git a/src/fileforge/core/datetime_utils.py b/src/fileforge/core/datetime_utils.py new file mode 100644 index 0000000..7c9864a --- /dev/null +++ b/src/fileforge/core/datetime_utils.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt + + +def serialize_datetime(v: dt.datetime) -> str: + """ + Serialize a datetime including timezone info. + + Uses the timezone info provided if present, otherwise uses the current runtime's timezone info. + + UTC datetimes end in "Z" while all other timezones are represented as offset from UTC, e.g. +05:00. + """ + + def _serialize_zoned_datetime(v: dt.datetime) -> str: + if v.tzinfo is not None and v.tzinfo.tzname(None) == dt.timezone.utc.tzname(None): + # UTC is a special case where we use "Z" at the end instead of "+00:00" + return v.isoformat().replace("+00:00", "Z") + else: + # Delegate to the typical +/- offset format + return v.isoformat() + + if v.tzinfo is not None: + return _serialize_zoned_datetime(v) + else: + local_tz = dt.datetime.now().astimezone().tzinfo + localized_dt = v.replace(tzinfo=local_tz) + return _serialize_zoned_datetime(localized_dt) diff --git a/src/fileforge/core/file.py b/src/fileforge/core/file.py new file mode 100644 index 0000000..cb0d40b --- /dev/null +++ b/src/fileforge/core/file.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +# File typing inspired by the flexibility of types within the httpx library +# https://github.com/encode/httpx/blob/master/httpx/_types.py +FileContent = typing.Union[typing.IO[bytes], bytes, str] +File = typing.Union[ + # file (or bytes) + FileContent, + # (filename, file (or bytes)) + typing.Tuple[typing.Optional[str], FileContent], + # (filename, file (or bytes), content_type) + typing.Tuple[typing.Optional[str], FileContent, typing.Optional[str]], + # (filename, file (or bytes), content_type, headers) + typing.Tuple[typing.Optional[str], FileContent, typing.Optional[str], typing.Mapping[str, str]], +] + + +def convert_file_dict_to_httpx_tuples( + d: typing.Dict[str, typing.Union[File, typing.List[File]]] +) -> typing.List[typing.Tuple[str, File]]: + """ + The format we use is a list of tuples, where the first element is the + name of the file and the second is the file object. Typically HTTPX wants + a dict, but to be able to send lists of files, you have to use the list + approach (which also works for non-lists) + https://github.com/encode/httpx/pull/1032 + """ + + httpx_tuples = [] + for key, file_like in d.items(): + if isinstance(file_like, list): + for file_like_item in file_like: + httpx_tuples.append((key, file_like_item)) + else: + httpx_tuples.append((key, file_like)) + return httpx_tuples diff --git a/src/fileforge/core/http_client.py b/src/fileforge/core/http_client.py new file mode 100644 index 0000000..4e6877d --- /dev/null +++ b/src/fileforge/core/http_client.py @@ -0,0 +1,130 @@ +# This file was auto-generated by Fern from our API Definition. + +import asyncio +import email.utils +import re +import time +import typing +from contextlib import asynccontextmanager, contextmanager +from functools import wraps +from random import random + +import httpx + +INITIAL_RETRY_DELAY_SECONDS = 0.5 +MAX_RETRY_DELAY_SECONDS = 10 +MAX_RETRY_DELAY_SECONDS_FROM_HEADER = 30 + + +def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]: + """ + This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait. + + Inspired by the urllib3 retry implementation. + """ + retry_after_ms = response_headers.get("retry-after-ms") + if retry_after_ms is not None: + try: + return int(retry_after_ms) / 1000 if retry_after_ms > 0 else 0 + except Exception: + pass + + retry_after = response_headers.get("retry-after") + if retry_after is None: + return None + + # Attempt to parse the header as an int. + if re.match(r"^\s*[0-9]+\s*$", retry_after): + seconds = float(retry_after) + # Fallback to parsing it as a date. + else: + retry_date_tuple = email.utils.parsedate_tz(retry_after) + if retry_date_tuple is None: + return None + if retry_date_tuple[9] is None: # Python 2 + # Assume UTC if no timezone was specified + # On Python2.7, parsedate_tz returns None for a timezone offset + # instead of 0 if no timezone is given, where mktime_tz treats + # a None timezone offset as local time. + retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:] + + retry_date = email.utils.mktime_tz(retry_date_tuple) + seconds = retry_date - time.time() + + if seconds < 0: + seconds = 0 + + return seconds + + +def _retry_timeout(response: httpx.Response, retries: int) -> float: + """ + Determine the amount of time to wait before retrying a request. + This function begins by trying to parse a retry-after header from the response, and then proceeds to use exponential backoff + with a jitter to determine the number of seconds to wait. + """ + + # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. + retry_after = _parse_retry_after(response.headers) + if retry_after is not None and retry_after <= MAX_RETRY_DELAY_SECONDS_FROM_HEADER: + return retry_after + + # Apply exponential backoff, capped at MAX_RETRY_DELAY_SECONDS. + retry_delay = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) + + # Add a randomness / jitter to the retry delay to avoid overwhelming the server with retries. + timeout = retry_delay * (1 - 0.25 * random()) + return timeout if timeout >= 0 else 0 + + +def _should_retry(response: httpx.Response) -> bool: + retriable_400s = [429, 408, 409] + return response.status_code >= 500 or response.status_code in retriable_400s + + +class HttpClient: + def __init__(self, *, httpx_client: httpx.Client): + self.httpx_client = httpx_client + + # Ensure that the signature of the `request` method is the same as the `httpx.Client.request` method + @wraps(httpx.Client.request) + def request( + self, *args: typing.Any, max_retries: int = 0, retries: int = 0, **kwargs: typing.Any + ) -> httpx.Response: + response = self.httpx_client.request(*args, **kwargs) + if _should_retry(response=response): + if max_retries > retries: + time.sleep(_retry_timeout(response=response, retries=retries)) + return self.request(max_retries=max_retries, retries=retries + 1, *args, **kwargs) + return response + + @wraps(httpx.Client.stream) + @contextmanager + def stream(self, *args: typing.Any, max_retries: int = 0, retries: int = 0, **kwargs: typing.Any) -> typing.Any: + with self.httpx_client.stream(*args, **kwargs) as stream: + yield stream + + +class AsyncHttpClient: + def __init__(self, *, httpx_client: httpx.AsyncClient): + self.httpx_client = httpx_client + + # Ensure that the signature of the `request` method is the same as the `httpx.Client.request` method + @wraps(httpx.AsyncClient.request) + async def request( + self, *args: typing.Any, max_retries: int = 0, retries: int = 0, **kwargs: typing.Any + ) -> httpx.Response: + response = await self.httpx_client.request(*args, **kwargs) + if _should_retry(response=response): + if max_retries > retries: + await asyncio.sleep(_retry_timeout(response=response, retries=retries)) + return await self.request(max_retries=max_retries, retries=retries + 1, *args, **kwargs) + return response + + @wraps(httpx.AsyncClient.stream) + @asynccontextmanager + async def stream( + self, *args: typing.Any, max_retries: int = 0, retries: int = 0, **kwargs: typing.Any + ) -> typing.Any: + async with self.httpx_client.stream(*args, **kwargs) as stream: + yield stream diff --git a/src/fileforge/core/jsonable_encoder.py b/src/fileforge/core/jsonable_encoder.py new file mode 100644 index 0000000..7f48273 --- /dev/null +++ b/src/fileforge/core/jsonable_encoder.py @@ -0,0 +1,99 @@ +# This file was auto-generated by Fern from our API Definition. + +""" +jsonable_encoder converts a Python object to a JSON-friendly dict +(e.g. datetimes to strings, Pydantic models to dicts). + +Taken from FastAPI, and made a bit simpler +https://github.com/tiangolo/fastapi/blob/master/fastapi/encoders.py +""" + +import dataclasses +import datetime as dt +from collections import defaultdict +from enum import Enum +from pathlib import PurePath +from types import GeneratorType +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union + +from .datetime_utils import serialize_datetime +from .pydantic_utilities import pydantic_v1 + +SetIntStr = Set[Union[int, str]] +DictIntStrAny = Dict[Union[int, str], Any] + + +def generate_encoders_by_class_tuples( + type_encoder_map: Dict[Any, Callable[[Any], Any]] +) -> Dict[Callable[[Any], Any], Tuple[Any, ...]]: + encoders_by_class_tuples: Dict[Callable[[Any], Any], Tuple[Any, ...]] = defaultdict(tuple) + for type_, encoder in type_encoder_map.items(): + encoders_by_class_tuples[encoder] += (type_,) + return encoders_by_class_tuples + + +encoders_by_class_tuples = generate_encoders_by_class_tuples(pydantic_v1.json.ENCODERS_BY_TYPE) + + +def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None) -> Any: + custom_encoder = custom_encoder or {} + if custom_encoder: + if type(obj) in custom_encoder: + return custom_encoder[type(obj)](obj) + else: + for encoder_type, encoder_instance in custom_encoder.items(): + if isinstance(obj, encoder_type): + return encoder_instance(obj) + if isinstance(obj, pydantic_v1.BaseModel): + encoder = getattr(obj.__config__, "json_encoders", {}) + if custom_encoder: + encoder.update(custom_encoder) + obj_dict = obj.dict(by_alias=True) + if "__root__" in obj_dict: + obj_dict = obj_dict["__root__"] + return jsonable_encoder(obj_dict, custom_encoder=encoder) + if dataclasses.is_dataclass(obj): + obj_dict = dataclasses.asdict(obj) + return jsonable_encoder(obj_dict, custom_encoder=custom_encoder) + if isinstance(obj, Enum): + return obj.value + if isinstance(obj, PurePath): + return str(obj) + if isinstance(obj, (str, int, float, type(None))): + return obj + if isinstance(obj, dt.datetime): + return serialize_datetime(obj) + if isinstance(obj, dt.date): + return str(obj) + if isinstance(obj, dict): + encoded_dict = {} + allowed_keys = set(obj.keys()) + for key, value in obj.items(): + if key in allowed_keys: + encoded_key = jsonable_encoder(key, custom_encoder=custom_encoder) + encoded_value = jsonable_encoder(value, custom_encoder=custom_encoder) + encoded_dict[encoded_key] = encoded_value + return encoded_dict + if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)): + encoded_list = [] + for item in obj: + encoded_list.append(jsonable_encoder(item, custom_encoder=custom_encoder)) + return encoded_list + + if type(obj) in pydantic_v1.json.ENCODERS_BY_TYPE: + return pydantic_v1.json.ENCODERS_BY_TYPE[type(obj)](obj) + for encoder, classes_tuple in encoders_by_class_tuples.items(): + if isinstance(obj, classes_tuple): + return encoder(obj) + + try: + data = dict(obj) + except Exception as e: + errors: List[Exception] = [] + errors.append(e) + try: + data = vars(obj) + except Exception as e: + errors.append(e) + raise ValueError(errors) from e + return jsonable_encoder(data, custom_encoder=custom_encoder) diff --git a/src/fileforge/core/pydantic_utilities.py b/src/fileforge/core/pydantic_utilities.py new file mode 100644 index 0000000..a72c1a5 --- /dev/null +++ b/src/fileforge/core/pydantic_utilities.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic + +IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + +if IS_PYDANTIC_V2: + import pydantic.v1 as pydantic_v1 # type: ignore # nopycln: import +else: + import pydantic as pydantic_v1 # type: ignore # nopycln: import + + +def deep_union_pydantic_dicts( + source: typing.Dict[str, typing.Any], destination: typing.Dict[str, typing.Any] +) -> typing.Dict[str, typing.Any]: + for key, value in source.items(): + if isinstance(value, dict): + node = destination.setdefault(key, {}) + deep_union_pydantic_dicts(value, node) + else: + destination[key] = value + + return destination + + +__all__ = ["pydantic_v1"] diff --git a/src/fileforge/core/query_encoder.py b/src/fileforge/core/query_encoder.py new file mode 100644 index 0000000..1f5f766 --- /dev/null +++ b/src/fileforge/core/query_encoder.py @@ -0,0 +1,33 @@ +# This file was auto-generated by Fern from our API Definition. + +from collections import ChainMap +from typing import Any, Dict, Optional + +from .pydantic_utilities import pydantic_v1 + + +# Flattens dicts to be of the form {"key[subkey][subkey2]": value} where value is not a dict +def traverse_query_dict(dict_flat: Dict[str, Any], key_prefix: Optional[str] = None) -> Dict[str, Any]: + result = {} + for k, v in dict_flat.items(): + key = f"{key_prefix}[{k}]" if key_prefix is not None else k + if isinstance(v, dict): + result.update(traverse_query_dict(v, key)) + else: + result[key] = v + return result + + +def single_query_encoder(query_key: str, query_value: Any) -> Dict[str, Any]: + if isinstance(query_value, pydantic_v1.BaseModel) or isinstance(query_value, dict): + if isinstance(query_value, pydantic_v1.BaseModel): + obj_dict = query_value.dict(by_alias=True) + else: + obj_dict = query_value + return traverse_query_dict(obj_dict, query_key) + + return {query_key: query_value} + + +def encode_query(query: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: + return dict(ChainMap(*[single_query_encoder(k, v) for k, v in query.items()])) if query is not None else None diff --git a/src/fileforge/core/remove_none_from_dict.py b/src/fileforge/core/remove_none_from_dict.py new file mode 100644 index 0000000..2da30f7 --- /dev/null +++ b/src/fileforge/core/remove_none_from_dict.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Optional + + +def remove_none_from_dict(original: Dict[str, Optional[Any]]) -> Dict[str, Any]: + new: Dict[str, Any] = {} + for key, value in original.items(): + if value is not None: + new[key] = value + return new diff --git a/src/fileforge/core/request_options.py b/src/fileforge/core/request_options.py new file mode 100644 index 0000000..cd6f27a --- /dev/null +++ b/src/fileforge/core/request_options.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +try: + from typing import NotRequired # type: ignore +except ImportError: + from typing_extensions import NotRequired # type: ignore + + +class RequestOptions(typing.TypedDict): + """ + Additional options for request-specific configuration when calling APIs via the SDK. + This is used primarily as an optional final parameter for service functions. + + Attributes: + - timeout_in_seconds: int. The number of seconds to await an API call before timing out. + + - max_retries: int. The max number of retries to attempt if the API call fails. + + - additional_headers: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's header dict + + - additional_query_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's query parameters dict + + - additional_body_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's body parameters dict + """ + + timeout_in_seconds: NotRequired[int] + max_retries: NotRequired[int] + additional_headers: NotRequired[typing.Dict[str, typing.Any]] + additional_query_parameters: NotRequired[typing.Dict[str, typing.Any]] + additional_body_parameters: NotRequired[typing.Dict[str, typing.Any]] diff --git a/src/fileforge/environment.py b/src/fileforge/environment.py new file mode 100644 index 0000000..494932c --- /dev/null +++ b/src/fileforge/environment.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import enum + + +class FileforgeEnvironment(enum.Enum): + DEFAULT = "https://api.fileforge.com" diff --git a/src/fileforge/errors/__init__.py b/src/fileforge/errors/__init__.py new file mode 100644 index 0000000..ade6099 --- /dev/null +++ b/src/fileforge/errors/__init__.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +from .bad_gateway_error import BadGatewayError +from .bad_request_error import BadRequestError +from .internal_server_error import InternalServerError +from .unauthorized_error import UnauthorizedError + +__all__ = ["BadGatewayError", "BadRequestError", "InternalServerError", "UnauthorizedError"] diff --git a/src/fileforge/errors/bad_gateway_error.py b/src/fileforge/errors/bad_gateway_error.py new file mode 100644 index 0000000..c483054 --- /dev/null +++ b/src/fileforge/errors/bad_gateway_error.py @@ -0,0 +1,9 @@ +# This file was auto-generated by Fern from our API Definition. + +from ..core.api_error import ApiError +from ..types.error_schema import ErrorSchema + + +class BadGatewayError(ApiError): + def __init__(self, body: ErrorSchema): + super().__init__(status_code=502, body=body) diff --git a/src/fileforge/errors/bad_request_error.py b/src/fileforge/errors/bad_request_error.py new file mode 100644 index 0000000..18e1d6b --- /dev/null +++ b/src/fileforge/errors/bad_request_error.py @@ -0,0 +1,9 @@ +# This file was auto-generated by Fern from our API Definition. + +from ..core.api_error import ApiError +from ..types.error_schema import ErrorSchema + + +class BadRequestError(ApiError): + def __init__(self, body: ErrorSchema): + super().__init__(status_code=400, body=body) diff --git a/src/fileforge/errors/internal_server_error.py b/src/fileforge/errors/internal_server_error.py new file mode 100644 index 0000000..c2764d6 --- /dev/null +++ b/src/fileforge/errors/internal_server_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError + + +class InternalServerError(ApiError): + def __init__(self, body: typing.Any): + super().__init__(status_code=500, body=body) diff --git a/src/fileforge/errors/unauthorized_error.py b/src/fileforge/errors/unauthorized_error.py new file mode 100644 index 0000000..0a134b4 --- /dev/null +++ b/src/fileforge/errors/unauthorized_error.py @@ -0,0 +1,9 @@ +# This file was auto-generated by Fern from our API Definition. + +from ..core.api_error import ApiError +from ..types.error_schema import ErrorSchema + + +class UnauthorizedError(ApiError): + def __init__(self, body: ErrorSchema): + super().__init__(status_code=401, body=body) diff --git a/src/fileforge/py.typed b/src/fileforge/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/fileforge/types/__init__.py b/src/fileforge/types/__init__.py new file mode 100644 index 0000000..29d2a1b --- /dev/null +++ b/src/fileforge/types/__init__.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +from .convert_docx_request_options import ConvertDocxRequestOptions +from .error_schema import ErrorSchema +from .generate_request_options import GenerateRequestOptions +from .merge_request_options import MergeRequestOptions + +__all__ = ["ConvertDocxRequestOptions", "ErrorSchema", "GenerateRequestOptions", "MergeRequestOptions"] diff --git a/src/fileforge/types/convert_docx_request_options.py b/src/fileforge/types/convert_docx_request_options.py new file mode 100644 index 0000000..b455e61 --- /dev/null +++ b/src/fileforge/types/convert_docx_request_options.py @@ -0,0 +1,62 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.datetime_utils import serialize_datetime +from ..core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 + + +class ConvertDocxRequestOptions(pydantic_v1.BaseModel): + """ + Conversion options. This field is required even if empty. + + **Options** + + - `templateLiterals`: Map of template literals to replace in the document. Template literals should be enclosed in double curly braces, e.g. `{{name}}`. Variables name can contain alphanumeric characters and hyphens. All variables are case-sensitive. The value for each variable should be a string. If a value of undefined is passed, the variable will not be removed from the document. If you need to remove a variable, pass an empty string as the value. + + **NB** variables should **not** have surrounding spaces, e.g. `{{ name }}`. + + **Example** + + In the Word document: `{{name}} {{nickname}}. was born on {{date}}.` + + ```json + { + "templateLiterals": { + "name": "John Doe", + "date": "2021-12-31", + "nickname": "" + } + } + ``` + + There will not be an error if a variable is not found in the document, nor if variables found in the document are not in the options. + """ + + template_literals: typing.Optional[typing.Dict[str, str]] = pydantic_v1.Field( + alias="templateLiterals", default=None + ) + """ + Map of template literals to replace in the document. + """ + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + kwargs_with_defaults_exclude_unset: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} + kwargs_with_defaults_exclude_none: typing.Any = {"by_alias": True, "exclude_none": True, **kwargs} + + return deep_union_pydantic_dicts( + super().dict(**kwargs_with_defaults_exclude_unset), super().dict(**kwargs_with_defaults_exclude_none) + ) + + class Config: + frozen = True + smart_union = True + allow_population_by_field_name = True + populate_by_name = True + extra = pydantic_v1.Extra.allow + json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/fileforge/types/error_schema.py b/src/fileforge/types/error_schema.py new file mode 100644 index 0000000..54219e5 --- /dev/null +++ b/src/fileforge/types/error_schema.py @@ -0,0 +1,48 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.datetime_utils import serialize_datetime +from ..core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 + + +class ErrorSchema(pydantic_v1.BaseModel): + """ + Generic error response schema + """ + + status_code: float = pydantic_v1.Field(alias="statusCode") + """ + The HTTP status code + """ + + code: str = pydantic_v1.Field() + """ + A machine-readable error code + """ + + message: str = pydantic_v1.Field() + """ + A human-readable message. This field may also provide additional context to the error code. + """ + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + kwargs_with_defaults_exclude_unset: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} + kwargs_with_defaults_exclude_none: typing.Any = {"by_alias": True, "exclude_none": True, **kwargs} + + return deep_union_pydantic_dicts( + super().dict(**kwargs_with_defaults_exclude_unset), super().dict(**kwargs_with_defaults_exclude_none) + ) + + class Config: + frozen = True + smart_union = True + allow_population_by_field_name = True + populate_by_name = True + extra = pydantic_v1.Extra.allow + json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/fileforge/types/generate_request_options.py b/src/fileforge/types/generate_request_options.py new file mode 100644 index 0000000..e7298db --- /dev/null +++ b/src/fileforge/types/generate_request_options.py @@ -0,0 +1,53 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.datetime_utils import serialize_datetime +from ..core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 + + +class GenerateRequestOptions(pydantic_v1.BaseModel): + """ + Conversion options. This field is required even if empty. + """ + + test: typing.Optional[bool] = pydantic_v1.Field(default=None) + """ + Generate a test document instead of a production document. The generated document will contain a watermark. Defaults to true. + """ + + host: typing.Optional[bool] = pydantic_v1.Field(default=None) + """ + If enabled, the document will be hosted by FileForge and a presigned URL will be returned. + """ + + expires_at: typing.Optional[dt.datetime] = pydantic_v1.Field(alias="expiresAt", default=None) + """ + If host is enabled, the expiration date of the presigned URL. Defaults to 7 days from now. Cannot exceed 7 days from now. + """ + + file_name: typing.Optional[str] = pydantic_v1.Field(alias="fileName", default=None) + """ + The name of the generated PDF file. Defaults to document. The file name should not contain extensions nor path traversals. + """ + + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + kwargs_with_defaults_exclude_unset: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} + kwargs_with_defaults_exclude_none: typing.Any = {"by_alias": True, "exclude_none": True, **kwargs} + + return deep_union_pydantic_dicts( + super().dict(**kwargs_with_defaults_exclude_unset), super().dict(**kwargs_with_defaults_exclude_none) + ) + + class Config: + frozen = True + smart_union = True + allow_population_by_field_name = True + populate_by_name = True + extra = pydantic_v1.Extra.allow + json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/fileforge/types/merge_request_options.py b/src/fileforge/types/merge_request_options.py new file mode 100644 index 0000000..b11ffeb --- /dev/null +++ b/src/fileforge/types/merge_request_options.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt +import typing + +from ..core.datetime_utils import serialize_datetime +from ..core.pydantic_utilities import deep_union_pydantic_dicts, pydantic_v1 + + +class MergeRequestOptions(pydantic_v1.BaseModel): + def json(self, **kwargs: typing.Any) -> str: + kwargs_with_defaults: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: typing.Any) -> typing.Dict[str, typing.Any]: + kwargs_with_defaults_exclude_unset: typing.Any = {"by_alias": True, "exclude_unset": True, **kwargs} + kwargs_with_defaults_exclude_none: typing.Any = {"by_alias": True, "exclude_none": True, **kwargs} + + return deep_union_pydantic_dicts( + super().dict(**kwargs_with_defaults_exclude_unset), super().dict(**kwargs_with_defaults_exclude_none) + ) + + class Config: + frozen = True + smart_union = True + extra = pydantic_v1.Extra.allow + json_encoders = {dt.datetime: serialize_datetime} diff --git a/src/fileforge/version.py b/src/fileforge/version.py new file mode 100644 index 0000000..d125d84 --- /dev/null +++ b/src/fileforge/version.py @@ -0,0 +1,4 @@ + +from importlib import metadata + +__version__ = metadata.version("fileforge") diff --git a/tests/custom/test_client.py b/tests/custom/test_client.py new file mode 100644 index 0000000..60a58e6 --- /dev/null +++ b/tests/custom/test_client.py @@ -0,0 +1,6 @@ +import pytest + +# Get started with writing tests with pytest at https://docs.pytest.org +@pytest.mark.skip(reason="Unimplemented") +def test_client() -> None: + assert True == True