From 19cea84afcc98d2ce1710ce7dccaf565a38aaad1 Mon Sep 17 00:00:00 2001 From: r-spiewak <63987228+r-spiewak@users.noreply.github.com> Date: Mon, 6 Apr 2026 10:49:43 -0400 Subject: [PATCH 01/25] Add poetry and pre-commit hooks --- .pylintrc | 645 ++++++++++++++++++++++ README.md | 11 + poetry.lock | 1400 ++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 156 ++++-- 4 files changed, 2182 insertions(+), 30 deletions(-) create mode 100644 .pylintrc create mode 100644 poetry.lock diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..2b65da3 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,645 @@ +[MAIN] +init-hook='import sys; sys.path.append("src")' + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint +# in a server-like mode. +clear-cache-post-run=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, +# it can't be used as an escape character. +ignore-paths= + src/stars/mcp_servers/docker/mcp-server-docker/.*, + src/stars/mcp_servers/pyiri/pyiridocker/.*, + lib/geostars/.*, + lib/eve-api/.* + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=^\.# + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + pylint_per_file_ignores + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +# py-version=3.12 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +source-roots= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type alias names. If left empty, type +# alias names will be checked with the set naming style. +#typealias-rgx= + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + asyncSetUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=50 + +# Maximum number of attributes for a class (see R0902). +max-attributes=70 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=120 + +# Maximum number of locals for function / method body. +max-locals=150 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException,builtins.Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + use-implicit-booleaness-not-comparison-to-string, + use-implicit-booleaness-not-comparison-to-zero, + ; duplicate-code, + docstring-first-line-empty + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + +per-file-ignores = + src/stars/mcp_servers/python_toolbox/**:duplicate-code, + src/stars/docs/conf.py:invalid-name + +[METHOD_ARGS] + +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + TODO, + XXX + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= + +# Set the output format. Available formats are: text, parseable, colorized, +# json2 (improved json format), json (old json format) and msvs (visual +# studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. No available dictionaries : You need to install +# both the python package and the system dependency for enchant to work. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io \ No newline at end of file diff --git a/README.md b/README.md index 6786224..193f67e 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,17 @@ Provides login, automatic JWT token refresh, and generic HTTP methods that retur ## Installation + + +For the development version: +```bash +poetry install +``` +or ```bash pip install -e ".[dev]" ``` diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..413c4f8 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1400 @@ +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. + +[[package]] +name = "anyio" +version = "4.13.0" +description = "High-level concurrency and networking framework on top of asyncio or Trio" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708"}, + {file = "anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +trio = ["trio (>=0.32.0)"] + +[[package]] +name = "astroid" +version = "3.3.11" +description = "An abstract syntax tree for Python with inference support." +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec"}, + {file = "astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} + +[[package]] +name = "autoflake" +version = "2.3.3" +description = "Removes unused imports and unused variables" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "autoflake-2.3.3-py3-none-any.whl", hash = "sha256:a51a3412aff16135ee5b3ec25922459fef10c1f23ce6d6c4977188df859e8b53"}, + {file = "autoflake-2.3.3.tar.gz", hash = "sha256:c24809541e23999f7a7b0d2faadf15deb0bc04cdde49728a2fd943a0c8055504"}, +] + +[package.dependencies] +pyflakes = ">=3.0.0" +tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." +optional = false +python-versions = "<3.11,>=3.8" +groups = ["dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, + {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, +] + +[[package]] +name = "black" +version = "23.12.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, + {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, + {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, + {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, + {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, + {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, + {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, + {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, + {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, + {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, + {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, + {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, + {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, + {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, + {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, + {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, + {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, + {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, + {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, + {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, + {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, + {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4) ; sys_platform != \"win32\" or implementation_name != \"pypy\"", "aiohttp (>=3.7.4,!=3.9.0) ; sys_platform == \"win32\" and implementation_name == \"pypy\""] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2026.2.25" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0"}, + {file = "cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-win32.whl", hash = "sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c"}, + {file = "charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d"}, + {file = "charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5"}, +] + +[[package]] +name = "click" +version = "8.3.2" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d"}, + {file = "click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" +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 = "coverage" +version = "7.13.5" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5"}, + {file = "coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930"}, + {file = "coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0"}, + {file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0"}, + {file = "coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58"}, + {file = "coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e"}, + {file = "coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d"}, + {file = "coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743"}, + {file = "coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd"}, + {file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8"}, + {file = "coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf"}, + {file = "coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9"}, + {file = "coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028"}, + {file = "coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01"}, + {file = "coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256"}, + {file = "coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf"}, + {file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c"}, + {file = "coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf"}, + {file = "coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810"}, + {file = "coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de"}, + {file = "coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1"}, + {file = "coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a"}, + {file = "coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6"}, + {file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17"}, + {file = "coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85"}, + {file = "coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b"}, + {file = "coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664"}, + {file = "coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d"}, + {file = "coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd"}, + {file = "coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479"}, + {file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2"}, + {file = "coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a"}, + {file = "coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819"}, + {file = "coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911"}, + {file = "coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f"}, + {file = "coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6"}, + {file = "coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0"}, + {file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0"}, + {file = "coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc"}, + {file = "coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633"}, + {file = "coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8"}, + {file = "coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b"}, + {file = "coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90"}, + {file = "coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea"}, + {file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a"}, + {file = "coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215"}, + {file = "coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43"}, + {file = "coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45"}, + {file = "coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61"}, + {file = "coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + +[[package]] +name = "detect-secrets" +version = "1.5.0" +description = "Tool for detecting secrets in the codebase" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "detect_secrets-1.5.0-py3-none-any.whl", hash = "sha256:e24e7b9b5a35048c313e983f76c4bd09dad89f045ff059e354f9943bf45aa060"}, + {file = "detect_secrets-1.5.0.tar.gz", hash = "sha256:6bb46dcc553c10df51475641bb30fd69d25645cc12339e46c824c1e0c388898a"}, +] + +[package.dependencies] +pyyaml = "*" +requests = "*" + +[package.extras] +gibberish = ["gibberish-detector"] +word-list = ["pyahocorasick"] + +[[package]] +name = "dill" +version = "0.4.1" +description = "serialize all of Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d"}, + {file = "dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] + +[[package]] +name = "distlib" +version = "0.4.0" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, + {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "filelock" +version = "3.25.2" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70"}, + {file = "filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694"}, +] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "identify" +version = "2.6.18" +description = "File identification library for Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "identify-2.6.18-py2.py3-none-any.whl", hash = "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737"}, + {file = "identify-2.6.18.tar.gz", hash = "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +groups = ["dev"] +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[package.extras] +colors = ["colorama (>=0.4.6)"] + +[[package]] +name = "librt" +version = "0.8.1" +description = "Mypyc runtime library" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc"}, + {file = "librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7"}, + {file = "librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6"}, + {file = "librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0"}, + {file = "librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b"}, + {file = "librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891"}, + {file = "librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7"}, + {file = "librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2"}, + {file = "librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd"}, + {file = "librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965"}, + {file = "librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da"}, + {file = "librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0"}, + {file = "librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e"}, + {file = "librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe"}, + {file = "librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb"}, + {file = "librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b"}, + {file = "librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9"}, + {file = "librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a"}, + {file = "librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9"}, + {file = "librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb"}, + {file = "librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d"}, + {file = "librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7"}, + {file = "librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0"}, + {file = "librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a"}, + {file = "librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444"}, + {file = "librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d"}, + {file = "librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35"}, + {file = "librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583"}, + {file = "librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c"}, + {file = "librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04"}, + {file = "librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363"}, + {file = "librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d"}, + {file = "librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a"}, + {file = "librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79"}, + {file = "librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0"}, + {file = "librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f"}, + {file = "librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c"}, + {file = "librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc"}, + {file = "librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c"}, + {file = "librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3"}, + {file = "librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78"}, + {file = "librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023"}, + {file = "librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730"}, + {file = "librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3"}, + {file = "librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1"}, + {file = "librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994"}, + {file = "librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a"}, + {file = "librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4"}, + {file = "librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61"}, + {file = "librt-0.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3dff3d3ca8db20e783b1bc7de49c0a2ab0b8387f31236d6a026597d07fcd68ac"}, + {file = "librt-0.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08eec3a1fc435f0d09c87b6bf1ec798986a3544f446b864e4099633a56fcd9ed"}, + {file = "librt-0.8.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e3f0a41487fd5fad7e760b9e8a90e251e27c2816fbc2cff36a22a0e6bcbbd9dd"}, + {file = "librt-0.8.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bacdb58d9939d95cc557b4dbaa86527c9db2ac1ed76a18bc8d26f6dc8647d851"}, + {file = "librt-0.8.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d7ab1f01aa753188605b09a51faa44a3327400b00b8cce424c71910fc0a128"}, + {file = "librt-0.8.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4998009e7cb9e896569f4be7004f09d0ed70d386fa99d42b6d363f6d200501ac"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2cc68eeeef5e906839c7bb0815748b5b0a974ec27125beefc0f942715785b551"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0bf69d79a23f4f40b8673a947a234baeeb133b5078b483b7297c5916539cf5d5"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:22b46eabd76c1986ee7d231b0765ad387d7673bbd996aa0d0d054b38ac65d8f6"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:237796479f4d0637d6b9cbcb926ff424a97735e68ade6facf402df4ec93375ed"}, + {file = "librt-0.8.1-cp39-cp39-win32.whl", hash = "sha256:4beb04b8c66c6ae62f8c1e0b2f097c1ebad9295c929a8d5286c05eae7c2fc7dc"}, + {file = "librt-0.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:64548cde61b692dc0dc379f4b5f59a2f582c2ebe7890d09c1ae3b9e66fa015b7"}, + {file = "librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mypy" +version = "1.20.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "mypy-1.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d99f515f95fd03a90875fdb2cca12ff074aa04490db4d190905851bdf8a549a8"}, + {file = "mypy-1.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bd0212976dc57a5bfeede7c219e7cd66568a32c05c9129686dd487c059c1b88a"}, + {file = "mypy-1.20.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8426d4d75d68714abc17a4292d922f6ba2cfb984b72c2278c437f6dae797865"}, + {file = "mypy-1.20.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02cca0761c75b42a20a2757ae58713276605eb29a08dd8a6e092aa347c4115ca"}, + {file = "mypy-1.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b3a49064504be59e59da664c5e149edc1f26c67c4f8e8456f6ba6aba55033018"}, + {file = "mypy-1.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:ebea00201737ad4391142808ed16e875add5c17f676e0912b387739f84991e13"}, + {file = "mypy-1.20.0-cp310-cp310-win_arm64.whl", hash = "sha256:e80cf77847d0d3e6e3111b7b25db32a7f8762fd4b9a3a72ce53fe16a2863b281"}, + {file = "mypy-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4525e7010b1b38334516181c5b81e16180b8e149e6684cee5a727c78186b4e3b"}, + {file = "mypy-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a17c5d0bdcca61ce24a35beb828a2d0d323d3fcf387d7512206888c900193367"}, + {file = "mypy-1.20.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75ff57defcd0f1d6e006d721ccdec6c88d4f6a7816eb92f1c4890d979d9ee62"}, + {file = "mypy-1.20.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b503ab55a836136b619b5fc21c8803d810c5b87551af8600b72eecafb0059cb0"}, + {file = "mypy-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1973868d2adbb4584a3835780b27436f06d1dc606af5be09f187aaa25be1070f"}, + {file = "mypy-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:2fcedb16d456106e545b2bfd7ef9d24e70b38ec252d2a629823a4d07ebcdb69e"}, + {file = "mypy-1.20.0-cp311-cp311-win_arm64.whl", hash = "sha256:379edf079ce44ac8d2805bcf9b3dd7340d4f97aad3a5e0ebabbf9d125b84b442"}, + {file = "mypy-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:002b613ae19f4ac7d18b7e168ffe1cb9013b37c57f7411984abbd3b817b0a214"}, + {file = "mypy-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9336b5e6712f4adaf5afc3203a99a40b379049104349d747eb3e5a3aa23ac2e"}, + {file = "mypy-1.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f13b3e41bce9d257eded794c0f12878af3129d80aacd8a3ee0dee51f3a978651"}, + {file = "mypy-1.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9804c3ad27f78e54e58b32e7cb532d128b43dbfb9f3f9f06262b821a0f6bd3f5"}, + {file = "mypy-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:697f102c5c1d526bdd761a69f17c6070f9892eebcb94b1a5963d679288c09e78"}, + {file = "mypy-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ecd63f75fdd30327e4ad8b5704bd6d91fc6c1b2e029f8ee14705e1207212489"}, + {file = "mypy-1.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:f194db59657c58593a3c47c6dfd7bad4ef4ac12dbc94d01b3a95521f78177e33"}, + {file = "mypy-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b20c8b0fd5877abdf402e79a3af987053de07e6fb208c18df6659f708b535134"}, + {file = "mypy-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:367e5c993ba34d5054d11937d0485ad6dfc60ba760fa326c01090fc256adf15c"}, + {file = "mypy-1.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f799d9db89fc00446f03281f84a221e50018fc40113a3ba9864b132895619ebe"}, + {file = "mypy-1.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:555658c611099455b2da507582ea20d2043dfdfe7f5ad0add472b1c6238b433f"}, + {file = "mypy-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:efe8d70949c3023698c3fca1e94527e7e790a361ab8116f90d11221421cd8726"}, + {file = "mypy-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:f49590891d2c2f8a9de15614e32e459a794bcba84693c2394291a2038bbaaa69"}, + {file = "mypy-1.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:76a70bf840495729be47510856b978f1b0ec7d08f257ca38c9d932720bf6b43e"}, + {file = "mypy-1.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0f42dfaab7ec1baff3b383ad7af562ab0de573c5f6edb44b2dab016082b89948"}, + {file = "mypy-1.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:31b5dbb55293c1bd27c0fc813a0d2bb5ceef9d65ac5afa2e58f829dab7921fd5"}, + {file = "mypy-1.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49d11c6f573a5a08f77fad13faff2139f6d0730ebed2cfa9b3d2702671dd7188"}, + {file = "mypy-1.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d3243c406773185144527f83be0e0aefc7bf4601b0b2b956665608bf7c98a83"}, + {file = "mypy-1.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a79c1eba7ac4209f2d850f0edd0a2f8bba88cbfdfefe6fb76a19e9d4fe5e71a2"}, + {file = "mypy-1.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:00e047c74d3ec6e71a2eb88e9ea551a2edb90c21f993aefa9e0d2a898e0bb732"}, + {file = "mypy-1.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:931a7630bba591593dcf6e97224a21ff80fb357e7982628d25e3c618e7f598ef"}, + {file = "mypy-1.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:26c8b52627b6552f47ff11adb4e1509605f094e29815323e487fc0053ebe93d1"}, + {file = "mypy-1.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:39362cdb4ba5f916e7976fccecaab1ba3a83e35f60fa68b64e9a70e221bb2436"}, + {file = "mypy-1.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34506397dbf40c15dc567635d18a21d33827e9ab29014fb83d292a8f4f8953b6"}, + {file = "mypy-1.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:555493c44a4f5a1b58d611a43333e71a9981c6dbe26270377b6f8174126a0526"}, + {file = "mypy-1.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2721f0ce49cb74a38f00c50da67cb7d36317b5eda38877a49614dc018e91c787"}, + {file = "mypy-1.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:47781555a7aa5fedcc2d16bcd72e0dc83eb272c10dd657f9fb3f9cc08e2e6abb"}, + {file = "mypy-1.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c70380fe5d64010f79fb863b9081c7004dd65225d2277333c219d93a10dad4dd"}, + {file = "mypy-1.20.0-py3-none-any.whl", hash = "sha256:a6e0641147cbfa7e4e94efdb95c2dab1aff8cfc159ded13e07f308ddccc8c48e"}, + {file = "mypy-1.20.0.tar.gz", hash = "sha256:eb96c84efcc33f0b5e0e04beacf00129dd963b67226b01c00b9dfc8affb464c3"}, +] + +[package.dependencies] +librt = {version = ">=0.8.0", markers = "platform_python_implementation != \"PyPy\""} +mypy_extensions = ">=1.0.0" +pathspec = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +native-parser = ["ast-serialize (>=0.1.1,<1.0.0)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"}, + {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}, +] + +[[package]] +name = "packaging" +version = "26.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, + {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, +] + +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] +tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] + +[[package]] +name = "platformdirs" +version = "4.9.4" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868"}, + {file = "platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "4.5.1" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77"}, + {file = "pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pyflakes" +version = "3.4.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, + {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, +] + +[[package]] +name = "pygments" +version = "2.20.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pylint" +version = "3.3.9" +description = "python code static checker" +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "pylint-3.3.9-py3-none-any.whl", hash = "sha256:01f9b0462c7730f94786c283f3e52a1fbdf0494bbe0971a78d7277ef46a751e7"}, + {file = "pylint-3.3.9.tar.gz", hash = "sha256:d312737d7b25ccf6b01cc4ac629b5dcd14a0fcf3ec392735ac70f137a9d5f83a"}, +] + +[package.dependencies] +astroid = ">=3.3.8,<=3.4.0.dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = [ + {version = ">=0.2", markers = "python_version < \"3.11\""}, + {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, + {version = ">=0.3.6", markers = "python_version == \"3.11\""}, +] +isort = ">=4.2.5,<5.13 || >5.13,<7" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2" +tomli = {version = ">=1.1", markers = "python_version < \"3.11\""} +tomlkit = ">=0.10.1" + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + +[[package]] +name = "pylint-per-file-ignores" +version = "3.2.1" +description = "A pylint plugin to ignore error codes per file." +optional = false +python-versions = "<4,>=3.10" +groups = ["dev"] +files = [ + {file = "pylint_per_file_ignores-3.2.1-py3-none-any.whl", hash = "sha256:aaac8b118791e742ccf7baaf42346978f6cd0440a9090d4087fc8ff26e4a31f2"}, + {file = "pylint_per_file_ignores-3.2.1.tar.gz", hash = "sha256:0a89f3cdc6fa09244a3f5624ad977ac9b026f0b25b2adb48c97c080da8d858f9"}, +] + +[package.dependencies] +pylint = ">=3.3,<5" + +[[package]] +name = "pytest" +version = "9.0.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"}, + {file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5"}, + {file = "pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5"}, +] + +[package.dependencies] +backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} +pytest = ">=8.2,<10" +typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"}, + {file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"}, +] + +[package.dependencies] +coverage = {version = ">=7.10.6", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=7" + +[package.extras] +testing = ["process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-env" +version = "1.6.0" +description = "pytest plugin that allows you to add environment variables." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest_env-1.6.0-py3-none-any.whl", hash = "sha256:1e7f8a62215e5885835daaed694de8657c908505b964ec8097a7ce77b403d9a3"}, + {file = "pytest_env-1.6.0.tar.gz", hash = "sha256:ac02d6fba16af54d61e311dd70a3c61024a4e966881ea844affc3c8f0bf207d3"}, +] + +[package.dependencies] +pytest = ">=9.0.2" +python-dotenv = ">=1.2.2" +tomli = {version = ">=2.4", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["covdefaults (>=2.3)", "coverage (>=7.13.4)", "pytest-mock (>=3.15.1)"] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d"}, + {file = "pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "pytest-testdox" +version = "3.1.0" +description = "A testdox format reporter for pytest" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-testdox-3.1.0.tar.gz", hash = "sha256:f48c49c517f0fb926560b383062db4961112078ec6ca555f91692c661bb5c765"}, + {file = "pytest_testdox-3.1.0-py2.py3-none-any.whl", hash = "sha256:f3a8f0789d668ccfb60f15aab81fb927b75066cfd19209176166bd7cecae73e6"}, +] + +[package.dependencies] +pytest = ">=4.6.0" + +[[package]] +name = "python-discovery" +version = "1.2.1" +description = "Python interpreter discovery" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "python_discovery-1.2.1-py3-none-any.whl", hash = "sha256:b6a957b24c1cd79252484d3566d1b49527581d46e789aaf43181005e56201502"}, + {file = "python_discovery-1.2.1.tar.gz", hash = "sha256:180c4d114bff1c32462537eac5d6a332b768242b76b69c0259c7d14b1b680c9e"}, +] + +[package.dependencies] +filelock = ">=3.15.4" +platformdirs = ">=4.3.6,<5" + +[package.extras] +docs = ["furo (>=2025.12.19)", "sphinx (>=9.1)", "sphinx-autodoc-typehints (>=3.6.3)", "sphinxcontrib-mermaid (>=2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.5.4)", "pytest (>=8.3.5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"}, + {file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "pyyaml" +version = "6.0.3" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, + {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, + {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, + {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, + {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, + {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, + {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, + {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, + {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, + {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, + {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, + {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, + {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, +] + +[[package]] +name = "requests" +version = "2.33.1" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a"}, + {file = "requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517"}, +] + +[package.dependencies] +certifi = ">=2023.5.7" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.26,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] + +[[package]] +name = "respx" +version = "0.22.0" +description = "A utility for mocking out the Python HTTPX and HTTP Core libraries." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0"}, + {file = "respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91"}, +] + +[package.dependencies] +httpx = ">=0.25.0" + +[[package]] +name = "tomli" +version = "2.4.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, + {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, + {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, + {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, + {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, + {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, + {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, + {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, + {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, + {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, + {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, + {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, + {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, + {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, + {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, + {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, + {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, + {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, +] + +[[package]] +name = "tomlkit" +version = "0.14.0" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680"}, + {file = "tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064"}, +] + +[[package]] +name = "types-requests" +version = "2.33.0.20260402" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "types_requests-2.33.0.20260402-py3-none-any.whl", hash = "sha256:c98372d7124dd5d10af815ee25c013897592ff92af27b27e22c98984102c3254"}, + {file = "types_requests-2.33.0.20260402.tar.gz", hash = "sha256:1bdd3ada9b869741c5c4b887d2c8b4e38284a1449751823b5ebbccba3eefd9da"}, +] + +[package.dependencies] +urllib3 = ">=2" + +[[package]] +name = "types-toml" +version = "0.10.8.20240310" +description = "Typing stubs for toml" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "types-toml-0.10.8.20240310.tar.gz", hash = "sha256:3d41501302972436a6b8b239c850b26689657e25281b48ff0ec06345b8830331"}, + {file = "types_toml-0.10.8.20240310-py3-none-any.whl", hash = "sha256:627b47775d25fa29977d9c70dc0cbab3f314f32c8d8d0c012f2ef5de7aaec05d"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] +markers = {main = "python_version <= \"3.12\""} + +[[package]] +name = "urllib3" +version = "2.6.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "virtualenv" +version = "21.2.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f"}, + {file = "virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""} +platformdirs = ">=3.9.1,<5" +python-discovery = ">=1" +typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} + +[metadata] +lock-version = "2.1" +python-versions = ">=3.10,<4" +content-hash = "84d8e6915efecb6116c920b87a7eac4578a28aee02d21f3732c71ae6b00c1798" diff --git a/pyproject.toml b/pyproject.toml index d820cf0..7c35e50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,16 +1,18 @@ -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - [project] -name = "eve-api" dynamic = ["version"] -description = "Minimal authenticated HTTP client for the EVE (Earth Virtual Expert) API" -readme = "README.md" +name = "eve-api" requires-python = ">=3.10" +keywords = ["earth-observation", "eve", "esa", "api-client"] + +[tool.poetry] +description = "Minimal authenticated HTTP client for the EVE (Earth Virtual Expert) API" license = "MIT" +readme = "README.md" authors = [ - { name = "GeoSTARS Team" } + "r-spiewak <63987228+r-spiewak@users.noreply.github.com>", + "dead-water ", + "rramosp", + "will-fawcett-trillium" ] classifiers = [ "Development Status :: 3 - Alpha", @@ -25,37 +27,131 @@ classifiers = [ "Topic :: Scientific/Engineering", "Framework :: AsyncIO", ] -keywords = ["earth-observation", "eve", "esa", "api-client"] - -dependencies = ["httpx>=0.27.0"] - -[project.optional-dependencies] -dev = [ - "pytest>=8.0.0", - "pytest-asyncio>=0.23.0", - "pytest-cov>=4.0.0", - "respx>=0.21.0", -] +packages = [{include = "eve_api", from = "src"}] +version = "0.0.0" # Placeholder; gets replaced dynamically by file [project.urls] Repository = "https://github.com/spaceml-org/eve-api" -[tool.hatch.version] -path = "src/eve_api/_version.py" +[tool.poetry.dependencies] +python = ">=3.10,<4" +httpx = ">=0.27.0" + +[tool.poetry.group.dev.dependencies] +pre-commit = "^4.5.1" +autoflake = "^2.2.1" +isort = "^5.13.2" +black = "^23.3.0" +mypy = "^1.8.0" +pylint = "^3.2.0" +coverage = "^7.4.1" +pytest = ">=8.0.0" +pytest-asyncio = ">=0.23.0" +pytest-cov = ">=4.0.0" +pytest-testdox = "^3.1.0" +pytest-mock = "^3.14.0" +detect-secrets = "^1.5.0" +types-requests = "^2.32.4.20250611" +pytest-env = "^1.1.5" +types-toml = "^0.10.8.20240310" +respx = ">=0.21.0" +pylint-per-file-ignores = "^3.2.0" + +[tool.black] +line-length = 79 +target-version = ['py311'] +include = '\.pyi?$' +extend-exclude = ''' +/( + # The following are specific to Black, you probably don't want those. + | blib2to3 + | tests/data + | profiling + | \.git + | __pycache__ + | \.tox + | \venv + | \.venv +)/ +''' -[tool.hatch.build.targets.wheel] -packages = ["src/eve_api"] +[tool.isort] +profile = "black" +line_length = 79 +skip_glob = [ + '*.parquet', +] +filter_files = true +skip_gitignore = true + +[tool.mypy] +exclude = [ + '\.yaml$', + '\.yml$', + '\.toml$', + '\.venv', +] +ignore_missing_imports = true [tool.pytest.ini_options] +pythonpath = [ + "src" +] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" testpaths = ["tests"] -addopts = "-v --tb=short" +addopts = "-v --tb=short --basetemp=/tmp/pytest" + +[tool.coverage.run] +source = ["src", "tests"] +omit = ["tests/**/conftest.py", "tests/*"] +data_file = "/tmp/eve_api/.coverage" + +[tool.coverage.report] +# Regexes for lines to exclude from consideration +exclude_also = [ + # Don't complain about missing debug-only code: + "def __repr__", + "if self\\.debug", + "if DEBUG_PRINT:", + "if DEBUG_PRINTS:", + + # Don't complain if tests don't hit defensive assertion code: + "raise AssertionError", + "raise NotImplementedError", + + # Don't complain if non-runnable code isn't run: + "if 0:", + "if __name__ == .__main__.:", -[tool.ruff] -line-length = 100 -target-version = "py310" + # Don't complain about abstract methods, they aren't run: + "@(abc\\.)?abstractmethod", + ] -[tool.ruff.lint] -select = ["E", "F", "I", "W"] -ignore = ["E501"] +ignore_errors = true + +[tool.poetry.requires-plugins] +poetry-dynamic-versioning = { version = ">=1.0.0,<2.0.0", extras = ["plugin"] } + +[tool.poetry-dynamic-versioning] +enable = true +vcs = "none" + +[tool.poetry-dynamic-versioning.from-file] +source = "src/eve_api/_version.py" +pattern = "^__version__\\s*=\\s*[\"']?(\\d+\\.\\d+\\.\\d+)[\"']?" + +# [tool.hatch.build.targets.wheel] +# packages = ["src/eve_api"] + +# [tool.ruff] +# line-length = 100 +# target-version = "py310" + +# [tool.ruff.lint] +# select = ["E", "F", "I", "W"] +# ignore = ["E501"] + +[build-system] +requires = ["poetry-core", "poetry-dynamic-versioning>=1.0.0,<2.0.0"] +build-backend = "poetry_dynamic_versioning.backend" From abf2369aa21b094782a2214fe7758698440826dc Mon Sep 17 00:00:00 2001 From: r-spiewak <63987228+r-spiewak@users.noreply.github.com> Date: Mon, 6 Apr 2026 10:55:34 -0400 Subject: [PATCH 02/25] feat: Add CI/CD workflow, add pre-commit config, add secrets baseline --- .github/workflows/main.yml | 27 ++++++++ .pre-commit-config.yaml | 81 +++++++++++++++++++++++ .secrets.baseline | 127 +++++++++++++++++++++++++++++++++++++ 3 files changed, 235 insertions(+) create mode 100644 .github/workflows/main.yml create mode 100644 .pre-commit-config.yaml create mode 100644 .secrets.baseline diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..62869a4 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,27 @@ +on: push +jobs: + test: + strategy: + matrix: + platform: [ubuntu-latest] + defaults: + run: + shell: bash + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12.11' + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + installer-parallel: true + - name: Install dependencies + run: | + poetry install --no-interaction + - name: Running pre-commit + uses: pre-commit/action@v3.0.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..53e8b57 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,81 @@ +repos: + - repo: local + hooks: + - id: detect-secrets + name: Detect secrets + language: system + entry: poetry run detect-secrets-hook + args: ['--baseline', '.secrets.baseline'] + exclude: '\.ipynb$' + - id: autoflake + name: autoflake + language: system + "types": [python] + require_serial: true + entry: poetry run autoflake + args: + - "--in-place" + - "--remove-unused-variables" + - "--recursive" + - id: black + name: black + entry: poetry run black . + language: system + types: [python] + - id: isort + name: isort + entry: poetry run isort . + language: system + exclude: | + (?x)^( + .+\.js$| + .+\.jsx$| + .+\.css$| + .+\.html$| + .+\.json$| + .+\.md$ + )$ + - id: mypy + name: mypy + entry: poetry run mypy src tests + pass_filenames: false + language: system + args: + - "--ignore-missing-imports" + - "--warn-unused-ignores" + - id: pylint + name: pylint + entry: poetry run pylint src tests + pass_filenames: false + language: system + args: + - "--enable-useless-suppression" + - id: pytest-with-coverage + name: Run pytest with coverage + entry: poetry run coverage run -m pytest --testdox tests + language: system + pass_filenames: false + always_run: true + - id: coverage-report + name: Coverage report + entry: poetry run coverage report + language: system + pass_filenames: false + verbose: true + args: + - "--fail-under=5" + - "--skip-empty" + - "--skip-covered" + - "--show-missing" + - id: sphinx-docs + name: Build Sphinx docs (GeoSTARS) + entry: bash -c 'command -v sphinx-build >/dev/null 2>&1 && poetry run sphinx-build -b html lib/geostars/docs _build/html/geostars || echo "sphinx-build not found, skipping"' + language: system + pass_filenames: false + always_run: true + - id: sphinx-docs-orchestrator + name: Build Sphinx docs (STARS) + entry: bash -c 'command -v sphinx-build >/dev/null 2>&1 && poetry run sphinx-build -b html src/stars/docs _build/html/orchestrator || echo "sphinx-build not found, skipping"' + language: system + pass_filenames: false + always_run: true diff --git a/.secrets.baseline b/.secrets.baseline new file mode 100644 index 0000000..b552b36 --- /dev/null +++ b/.secrets.baseline @@ -0,0 +1,127 @@ +{ + "version": "1.5.0", + "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, + { + "name": "AWSKeyDetector" + }, + { + "name": "AzureStorageKeyDetector" + }, + { + "name": "Base64HighEntropyString", + "limit": 4.5 + }, + { + "name": "BasicAuthDetector" + }, + { + "name": "CloudantDetector" + }, + { + "name": "DiscordBotTokenDetector" + }, + { + "name": "GitHubTokenDetector" + }, + { + "name": "GitLabTokenDetector" + }, + { + "name": "HexHighEntropyString", + "limit": 3.0 + }, + { + "name": "IbmCloudIamDetector" + }, + { + "name": "IbmCosHmacDetector" + }, + { + "name": "IPPublicDetector" + }, + { + "name": "JwtTokenDetector" + }, + { + "name": "KeywordDetector", + "keyword_exclude": "" + }, + { + "name": "MailchimpDetector" + }, + { + "name": "NpmDetector" + }, + { + "name": "OpenAIDetector" + }, + { + "name": "PrivateKeyDetector" + }, + { + "name": "PypiTokenDetector" + }, + { + "name": "SendGridDetector" + }, + { + "name": "SlackDetector" + }, + { + "name": "SoftlayerDetector" + }, + { + "name": "SquareOAuthDetector" + }, + { + "name": "StripeDetector" + }, + { + "name": "TelegramBotTokenDetector" + }, + { + "name": "TwilioKeyDetector" + } + ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_lock_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_swagger_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + } + ], + "results": {}, + "generated_at": "2025-07-08T20:25:49Z" +} From 5cd0a7fe903a7616ddf6dd691871d0cc8daf6592 Mon Sep 17 00:00:00 2001 From: r-spiewak <63987228+r-spiewak@users.noreply.github.com> Date: Mon, 6 Apr 2026 14:42:37 -0400 Subject: [PATCH 03/25] feat: Pass checks --- .pre-commit-config.yaml | 10 +- .pylintrc | 12 +- src/eve_api/__init__.py | 8 +- src/eve_api/auth.py | 48 ++++--- src/eve_api/client.py | 64 +++++---- src/eve_api/exceptions.py | 30 ++-- src/eve_api/response.py | 15 ++ tests/conftest.py | 25 ++-- tests/test_client.py | 294 +++++++++++++++++++++++++++----------- 9 files changed, 339 insertions(+), 167 deletions(-) create mode 100644 src/eve_api/response.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 53e8b57..1653558 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -68,14 +68,8 @@ repos: - "--skip-covered" - "--show-missing" - id: sphinx-docs - name: Build Sphinx docs (GeoSTARS) - entry: bash -c 'command -v sphinx-build >/dev/null 2>&1 && poetry run sphinx-build -b html lib/geostars/docs _build/html/geostars || echo "sphinx-build not found, skipping"' - language: system - pass_filenames: false - always_run: true - - id: sphinx-docs-orchestrator - name: Build Sphinx docs (STARS) - entry: bash -c 'command -v sphinx-build >/dev/null 2>&1 && poetry run sphinx-build -b html src/stars/docs _build/html/orchestrator || echo "sphinx-build not found, skipping"' + name: Build Sphinx docs + entry: bash -c 'command -v sphinx-build >/dev/null 2>&1 && poetry run sphinx-build -b html src/eve_api/docs _build/html/eve_api || echo "sphinx-build not found, skipping"' language: system pass_filenames: false always_run: true diff --git a/.pylintrc b/.pylintrc index 2b65da3..538af09 100644 --- a/.pylintrc +++ b/.pylintrc @@ -53,11 +53,7 @@ ignore=CVS # ignore-list. The regex matches against paths and can be in Posix or Windows # format. Because '\\' represents the directory delimiter on Windows systems, # it can't be used as an escape character. -ignore-paths= - src/stars/mcp_servers/docker/mcp-server-docker/.*, - src/stars/mcp_servers/pyiri/pyiridocker/.*, - lib/geostars/.*, - lib/eve-api/.* +# ignore-paths= # Files or directories matching the regular expression patterns are skipped. # The regex matches against base names, not paths. The default value ignores @@ -436,9 +432,11 @@ disable=raw-checker-failed, deprecated-pragma, use-symbolic-message-instead, use-implicit-booleaness-not-comparison-to-string, - use-implicit-booleaness-not-comparison-to-zero, + use-implicit-booleaness-not-comparison-to-zero + ; use-implicit-booleaness-not-comparison-to-zero, + ; docstring-first-line-empty + ; docstring-first-line-empty, ; duplicate-code, - docstring-first-line-empty # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/src/eve_api/__init__.py b/src/eve_api/__init__.py index fa3671d..748d649 100644 --- a/src/eve_api/__init__.py +++ b/src/eve_api/__init__.py @@ -1,8 +1,8 @@ """eve-api: Minimal authenticated HTTP client for the EVE API.""" -from eve_api._version import __version__ -from eve_api.client import EVEClient -from eve_api.exceptions import ( +from ._version import __version__ +from .client import EVEClient +from .exceptions import ( APIError, AuthenticationError, EVEError, @@ -14,6 +14,7 @@ TokenExpiredError, ValidationError, ) +from .response import EveApiResponse __all__ = [ "__version__", @@ -21,6 +22,7 @@ "EVEError", "APIError", "AuthenticationError", + "EveApiResponse", "ForbiddenError", "NotAuthenticatedError", "NotFoundError", diff --git a/src/eve_api/auth.py b/src/eve_api/auth.py index 1323455..534d658 100644 --- a/src/eve_api/auth.py +++ b/src/eve_api/auth.py @@ -7,7 +7,12 @@ import httpx -from eve_api.exceptions import AuthenticationError, NotAuthenticatedError, TokenExpiredError +from .exceptions import ( + AuthenticationError, + NotAuthenticatedError, + TokenExpiredError, +) +from .response import EveApiResponse class EVEAuth: @@ -75,17 +80,17 @@ async def login(self, email: str, password: str) -> None: client = await self._get_client() should_close = self._http_client is None - try: + try: # pylint: disable=too-many-try-statements response = await client.post( "/login", json={"email": email, "password": password}, ) - if response.status_code == 401: + if response.status_code == EveApiResponse.INVALID_CREDS.value: raise AuthenticationError("Invalid email or password") - elif response.status_code == 403: + if response.status_code == EveApiResponse.FORBIDDEN.value: raise AuthenticationError("Account not activated") - elif response.status_code != 200: + if response.status_code != EveApiResponse.SUCCESS.value: self._handle_error_response(response) data = response.json() @@ -103,30 +108,36 @@ async def refresh(self) -> None: NotAuthenticatedError: If no refresh token is available. """ if not self.refresh_token: - raise NotAuthenticatedError("No refresh token available. Please login first.") + raise NotAuthenticatedError( + "No refresh token available. Please login first." + ) client = await self._get_client() should_close = self._http_client is None - try: + try: # pylint: disable=too-many-try-statements response = await client.post( "/refresh", json={"refresh_token": self.refresh_token}, ) - if response.status_code == 401: + if response.status_code == EveApiResponse.INVALID_CREDS.value: # Refresh token expired self.access_token = None self.refresh_token = None self._token_expiry = None - raise TokenExpiredError("Refresh token expired. Please login again.") - elif response.status_code != 200: + raise TokenExpiredError( + "Refresh token expired. Please login again." + ) + if response.status_code != EveApiResponse.SUCCESS.value: self._handle_error_response(response) data = response.json() self.access_token = data.get("access_token") # Update expiry time - self._token_expiry = datetime.now(timezone.utc) + self._DEFAULT_EXPIRY + self._token_expiry = ( + datetime.now(timezone.utc) + self._DEFAULT_EXPIRY + ) finally: if should_close: @@ -142,7 +153,9 @@ def get_headers(self) -> dict[str, str]: NotAuthenticatedError: If not logged in. """ if not self.access_token: - raise NotAuthenticatedError("Not authenticated. Please login first.") + raise NotAuthenticatedError( + "Not authenticated. Please login first." + ) return {"Authorization": f"Bearer {self.access_token}"} async def ensure_authenticated(self) -> None: @@ -157,7 +170,9 @@ async def ensure_authenticated(self) -> None: TokenExpiredError: If token refresh fails. """ if not self.access_token: - raise NotAuthenticatedError("Not authenticated. Please login first.") + raise NotAuthenticatedError( + "Not authenticated. Please login first." + ) if self._should_refresh(): await self.refresh() @@ -193,7 +208,8 @@ def _store_tokens(self, data: dict[str, Any]) -> None: # Set expiry time (default 1 hour from now) self._token_expiry = datetime.now(timezone.utc) + self._DEFAULT_EXPIRY - def _handle_error_response(self, response: httpx.Response) -> None: + @staticmethod + def _handle_error_response(response: httpx.Response) -> None: """Handle error responses from auth endpoints. Args: @@ -202,10 +218,10 @@ def _handle_error_response(self, response: httpx.Response) -> None: Raises: AuthenticationError: With details from response. """ - try: + try: # pylint: disable=too-many-try-statements data = response.json() message = data.get("detail", str(data)) - except Exception: + except Exception: # pylint: disable=broad-exception-caught message = response.text or f"HTTP {response.status_code}" raise AuthenticationError(f"Authentication failed: {message}") diff --git a/src/eve_api/client.py b/src/eve_api/client.py index 9263547..09adaa4 100644 --- a/src/eve_api/client.py +++ b/src/eve_api/client.py @@ -3,12 +3,13 @@ from __future__ import annotations import json as _json -from typing import Any, AsyncIterator +from collections.abc import AsyncIterator +from typing import Any import httpx -from eve_api.auth import EVEAuth -from eve_api.exceptions import ( +from .auth import EVEAuth +from .exceptions import ( APIError, ForbiddenError, NotFoundError, @@ -16,6 +17,7 @@ StreamError, ValidationError, ) +from .response import EveApiResponse class EVEClient: @@ -140,7 +142,9 @@ async def get( Returns: Parsed JSON response (dict or list). """ - response = await self.request("GET", path, params=params, timeout=timeout) + response = await self.request( + "GET", path, params=params, timeout=timeout + ) return response.json() async def post( @@ -161,7 +165,9 @@ async def post( Returns: Parsed JSON response (dict or list). """ - response = await self.request("POST", path, json=json, params=params, timeout=timeout) + response = await self.request( + "POST", path, json=json, params=params, timeout=timeout + ) return response.json() async def patch( @@ -182,7 +188,9 @@ async def patch( Returns: Parsed JSON response (dict or list). """ - response = await self.request("PATCH", path, json=json, params=params, timeout=timeout) + response = await self.request( + "PATCH", path, json=json, params=params, timeout=timeout + ) return response.json() async def delete( @@ -201,8 +209,10 @@ async def delete( Returns: Parsed JSON response (dict or list), or None for 204 responses. """ - response = await self.request("DELETE", path, params=params, timeout=timeout) - if response.status_code == 204: + response = await self.request( + "DELETE", path, params=params, timeout=timeout + ) + if response.status_code == EveApiResponse.SUCCESS_NO_RESPONSE.value: return None return response.json() @@ -242,7 +252,7 @@ async def stream( headers=headers, timeout=timeout or 300.0, ) as response: - if response.status_code >= 400: + if response.status_code >= EveApiResponse.BAD_REQUEST.value: await response.aread() self._handle_error(response) @@ -250,25 +260,29 @@ async def stream( if not line or not line.startswith("data: "): continue - data_str = line[6:] - if data_str == "[DONE]": + if ( # pylint: disable=magic-value-comparison + data_str := line[6:] + ) == "[DONE]": return try: event = _json.loads(data_str) except _json.JSONDecodeError as e: - raise StreamError(f"Failed to parse SSE data: {e}") + raise StreamError(f"Failed to parse SSE data: {e}") from e yield event # Stop on terminal events - event_type = event.get("type", "") - if event_type in ("final", "error", "stopped"): + if event.get("type", "") in { + "final", + "error", + "stopped", + }: return # Low-level request method - async def request( + async def request( # pylint: disable=too-many-positional-arguments self, method: str, path: str, @@ -312,12 +326,13 @@ async def request( timeout=timeout or self._timeout, ) - if response.status_code >= 400: + if response.status_code >= EveApiResponse.BAD_REQUEST.value: self._handle_error(response) return response - def _handle_error(self, response: httpx.Response) -> None: + @staticmethod + def _handle_error(response: httpx.Response) -> None: """Handle error responses. Args: @@ -332,21 +347,20 @@ def _handle_error(self, response: httpx.Response) -> None: """ status = response.status_code - try: + try: # pylint: disable=too-many-try-statements data = response.json() message = data.get("detail", str(data)) if isinstance(message, list): message = "; ".join(str(e) for e in message) - except Exception: + except Exception: # pylint: disable=broad-exception-caught message = response.text or f"HTTP {status}" - if status == 404: + if status == EveApiResponse.NOT_FOUND.value: raise NotFoundError("resource", "unknown") - elif status == 403: + if status == EveApiResponse.FORBIDDEN.value: raise ForbiddenError(message) - elif status == 400: + if status == EveApiResponse.BAD_REQUEST.value: raise ValidationError(message) - elif status >= 500: + if status >= EveApiResponse.INTERNAL_SERVER_ERROR.value: raise ServerError(message, status_code=status) - else: - raise APIError(message, status_code=status) + raise APIError(message, status_code=status) diff --git a/src/eve_api/exceptions.py b/src/eve_api/exceptions.py index eec71a1..1d0bc29 100644 --- a/src/eve_api/exceptions.py +++ b/src/eve_api/exceptions.py @@ -4,11 +4,15 @@ from typing import Any +from .response import EveApiResponse + class EVEError(Exception): """Base exception for all EVE API client errors.""" - def __init__(self, message: str, details: dict[str, Any] | None = None) -> None: + def __init__( + self, message: str, details: dict[str, Any] | None = None + ) -> None: """Initialise the error. Args: @@ -23,20 +27,14 @@ def __init__(self, message: str, details: dict[str, Any] | None = None) -> None: class AuthenticationError(EVEError): """Raised when authentication fails.""" - pass - class TokenExpiredError(AuthenticationError): """Raised when the access token has expired and refresh failed.""" - pass - class NotAuthenticatedError(AuthenticationError): """Raised when attempting an operation that requires authentication.""" - pass - class APIError(EVEError): """Raised when the API returns an error response.""" @@ -70,7 +68,7 @@ def __init__(self, resource: str, resource_id: str) -> None: """ super().__init__( f"{resource.title()} not found: {resource_id}", - status_code=404, + status_code=EveApiResponse.NOT_FOUND.value, details={"resource": resource, "id": resource_id}, ) @@ -84,20 +82,26 @@ def __init__(self, message: str = "Access forbidden") -> None: Args: message: Human-readable error message. """ - super().__init__(message, status_code=403) + super().__init__(message, status_code=EveApiResponse.FORBIDDEN.value) class ValidationError(APIError): """Raised when request validation fails (400).""" - def __init__(self, message: str, details: dict[str, Any] | None = None) -> None: + def __init__( + self, message: str, details: dict[str, Any] | None = None + ) -> None: """Initialise the validation error. Args: message: Human-readable error message. details: Validation error details. """ - super().__init__(message, status_code=400, details=details) + super().__init__( + message, + status_code=EveApiResponse.BAD_REQUEST.value, + details=details, + ) class ServerError(APIError): @@ -106,7 +110,7 @@ class ServerError(APIError): def __init__( self, message: str = "Internal server error", - status_code: int = 500, + status_code: int = EveApiResponse.INTERNAL_SERVER_ERROR.value, details: dict[str, Any] | None = None, ) -> None: """Initialise the server error. @@ -121,5 +125,3 @@ def __init__( class StreamError(EVEError): """Raised when an error occurs during streaming.""" - - pass diff --git a/src/eve_api/response.py b/src/eve_api/response.py new file mode 100644 index 0000000..9a610b6 --- /dev/null +++ b/src/eve_api/response.py @@ -0,0 +1,15 @@ +"""This module contains an enum for response codes from EVE.""" + +from enum import Enum + + +class EveApiResponse(Enum): + """Response code from EVE API.""" + + SUCCESS = 200 + SUCCESS_NO_RESPONSE = 204 + BAD_REQUEST = 400 + INVALID_CREDS = 401 + FORBIDDEN = 403 + NOT_FOUND = 404 + INTERNAL_SERVER_ERROR = 500 diff --git a/tests/conftest.py b/tests/conftest.py index 060d142..809b878 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,25 +14,32 @@ def base_url() -> str: @pytest.fixture -def mock_api(base_url: str): +def mock_api(base_url): # pylint: disable=redefined-outer-name """Create a mock API context for testing.""" with respx.mock(base_url=base_url, assert_all_called=False) as mock: yield mock @pytest.fixture -async def client(base_url: str): +async def client(base_url: str): # pylint: disable=redefined-outer-name """Create an EVE client for testing.""" - async with EVEClient(base_url) as client: - yield client + async with EVEClient(base_url) as eve_client: + yield eve_client @pytest.fixture -async def authenticated_client(mock_api, client: EVEClient): +async def authenticated_client( + mock_api, client: EVEClient +): # pylint: disable=redefined-outer-name """Create an authenticated EVE client for testing.""" - mock_api.post("/login").mock(return_value=Response(200, json={ - "access_token": "test-access-token", - "refresh_token": "test-refresh-token", - })) + mock_api.post("/login").mock( + return_value=Response( + 200, + json={ + "access_token": "test-access-token", + "refresh_token": "test-refresh-token", + }, + ) + ) await client.login("test@example.com", "password") return client diff --git a/tests/test_client.py b/tests/test_client.py index 6a70c16..0ef63f4 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -3,42 +3,53 @@ import json import pytest -import respx from httpx import Response -from eve_api import ( +from eve_api import ( # StreamError, APIError, AuthenticationError, + EveApiResponse, EVEClient, ForbiddenError, NotAuthenticatedError, NotFoundError, ServerError, - StreamError, ValidationError, ) - # --- Authentication --- async def test_login_success(mock_api, client: EVEClient): - mock_api.post("/login").mock(return_value=Response(200, json={ - "access_token": "tok-123", - "refresh_token": "ref-456", - })) + """Test login success""" + tok = "tok-123" + mock_api.post("/login").mock( + return_value=Response( + 200, + json={ + "access_token": tok, + "refresh_token": "ref-456", + }, + ) + ) await client.login("user@example.com", "password") assert client.is_authenticated() - assert client.token == "tok-123" - assert client.auth_headers == {"Authorization": "Bearer tok-123"} + assert client.token == tok + assert client.auth_headers == {"Authorization": f"Bearer {tok}"} async def test_login_invalid_credentials(mock_api, client: EVEClient): - mock_api.post("/login").mock(return_value=Response(401, json={ - "detail": "Invalid credentials", - })) + """Test login for invalid credentials raises AuthenticationError""" + mock_api.post("/login").mock( + return_value=Response( + 401, + json={ + "detail": "Invalid credentials", + }, + ) + ) with pytest.raises(AuthenticationError, match="Invalid email or password"): await client.login("bad@example.com", "wrong") @@ -47,20 +58,28 @@ async def test_login_invalid_credentials(mock_api, client: EVEClient): async def test_login_account_not_activated(mock_api, client: EVEClient): - mock_api.post("/login").mock(return_value=Response(403, json={ - "detail": "Account not activated", - })) + """Test login for non-activated account raises AuthenticationError""" + mock_api.post("/login").mock( + return_value=Response( + 403, + json={ + "detail": "Account not activated", + }, + ) + ) with pytest.raises(AuthenticationError, match="Account not activated"): await client.login("user@example.com", "password") async def test_not_authenticated_error(client: EVEClient): + """Test that before login NotAuthenticatedError is raised""" with pytest.raises(NotAuthenticatedError): await client.get("/users/me") async def test_token_property_none_before_login(client: EVEClient): + """Test that the token attribute before login is None""" assert client.token is None @@ -68,64 +87,99 @@ async def test_token_property_none_before_login(client: EVEClient): async def test_get(mock_api, authenticated_client: EVEClient): - mock_api.get("/users/me").mock(return_value=Response(200, json={ - "id": "user-1", - "email": "test@example.com", - })) + """Test GET""" + user_id = "user-1" + user_email = "test@example.com" + mock_api.get("/users/me").mock( + return_value=Response( + 200, + json={ + "id": user_id, + "email": user_email, + }, + ) + ) data = await authenticated_client.get("/users/me") - assert data["id"] == "user-1" - assert data["email"] == "test@example.com" + assert data["id"] == user_id + assert data["email"] == user_email async def test_get_with_params(mock_api, authenticated_client: EVEClient): - mock_api.get("/collections/public").mock(return_value=Response(200, json={ - "data": [{"id": "c-1", "name": "Public"}], - "meta": {"total_count": 1}, - })) + """Test GET with params""" + data_name = "Public" + mock_api.get("/collections/public").mock( + return_value=Response( + 200, + json={ + "data": [{"id": "c-1", "name": data_name}], + "meta": {"total_count": 1}, + }, + ) + ) - data = await authenticated_client.get("/collections/public", params={"page": 1}) + data = await authenticated_client.get( + "/collections/public", params={"page": 1} + ) assert len(data["data"]) == 1 - assert data["data"][0]["name"] == "Public" + assert data["data"][0]["name"] == data_name # --- POST --- async def test_post(mock_api, authenticated_client: EVEClient): - mock_api.post("/conversations").mock(return_value=Response(201, json={ - "id": "conv-1", - "name": "New Chat", - })) + """Test POST""" + chat_id = "conv-1" + chat_name = "New Chat" + mock_api.post("/conversations").mock( + return_value=Response( + 201, + json={ + "id": chat_id, + "name": "New Chat", + }, + ) + ) - data = await authenticated_client.post("/conversations", json={"name": "New Chat"}) + data = await authenticated_client.post( + "/conversations", json={"name": chat_name} + ) - assert data["id"] == "conv-1" - assert data["name"] == "New Chat" + assert data["id"] == chat_id + assert data["name"] == chat_name # --- PATCH --- async def test_patch(mock_api, authenticated_client: EVEClient): - mock_api.patch("/conversations/conv-1").mock(return_value=Response(200, json={ - "id": "conv-1", - "name": "Renamed", - })) + """Test PATCH""" + new_name = "Renamed" + mock_api.patch("/conversations/conv-1").mock( + return_value=Response( + EveApiResponse.SUCCESS.value, + json={ + "id": "conv-1", + "name": new_name, + }, + ) + ) data = await authenticated_client.patch( - "/conversations/conv-1", json={"name": "Renamed"} + "/conversations/conv-1", json={"name": new_name} ) - assert data["name"] == "Renamed" + assert data["name"] == new_name # --- DELETE --- async def test_delete(mock_api, authenticated_client: EVEClient): + """Test deleting a conversation works when a response body is not included""" mock_api.delete("/conversations/conv-1").mock(return_value=Response(204)) result = await authenticated_client.delete("/conversations/conv-1") @@ -134,9 +188,15 @@ async def test_delete(mock_api, authenticated_client: EVEClient): async def test_delete_with_body(mock_api, authenticated_client: EVEClient): - mock_api.delete("/conversations/conv-1").mock(return_value=Response(200, json={ - "deleted": True, - })) + """Test deleting a conversation works when including a response body""" + mock_api.delete("/conversations/conv-1").mock( + return_value=Response( + EveApiResponse.SUCCESS.value, + json={ + "deleted": True, + }, + ) + ) result = await authenticated_client.delete("/conversations/conv-1") @@ -147,62 +207,114 @@ async def test_delete_with_body(mock_api, authenticated_client: EVEClient): async def test_request_no_auth(mock_api, client: EVEClient): - mock_api.get("/health").mock(return_value=Response(200, json={"status": "ok"})) + """Test EVEClient when auth_required=False""" + status = "ok" + mock_api.get("/health").mock( + return_value=Response( + EveApiResponse.SUCCESS.value, json={"status": status} + ) + ) response = await client.request("GET", "/health", auth_required=False) - assert response.status_code == 200 - assert response.json()["status"] == "ok" + assert response.status_code == EveApiResponse.SUCCESS.value + assert response.json()["status"] == status # --- Error handling --- async def test_404_raises_not_found(mock_api, authenticated_client: EVEClient): - mock_api.get("/conversations/missing").mock(return_value=Response(404, json={ - "detail": "Not found", - })) + """Test that 404 response code raises NotFoundError""" + mock_api.get("/conversations/missing").mock( + return_value=Response( + EveApiResponse.NOT_FOUND.value, + json={ + "detail": "Not found", + }, + ) + ) - with pytest.raises(NotFoundError): + with pytest.raises(NotFoundError) as exc_info: await authenticated_client.get("/conversations/missing") + assert exc_info.value.status_code == EveApiResponse.NOT_FOUND.value + async def test_403_raises_forbidden(mock_api, authenticated_client: EVEClient): - mock_api.get("/admin/users").mock(return_value=Response(403, json={ - "detail": "Forbidden", - })) + """Test that 403 response code raises ForbiddenError""" + mock_api.get("/admin/users").mock( + return_value=Response( + EveApiResponse.FORBIDDEN.value, + json={ + "detail": "Forbidden", + }, + ) + ) - with pytest.raises(ForbiddenError): + with pytest.raises(ForbiddenError) as exc_info: await authenticated_client.get("/admin/users") + assert exc_info.value.status_code == EveApiResponse.FORBIDDEN.value + -async def test_400_raises_validation_error(mock_api, authenticated_client: EVEClient): - mock_api.post("/conversations").mock(return_value=Response(400, json={ - "detail": "name is required", - })) +async def test_400_raises_validation_error( + mock_api, authenticated_client: EVEClient +): + """Test that 400 response code raises ValidationError""" + mock_api.post("/conversations").mock( + return_value=Response( + EveApiResponse.BAD_REQUEST.value, + json={ + "detail": "name is required", + }, + ) + ) - with pytest.raises(ValidationError, match="name is required"): + with pytest.raises(ValidationError, match="name is required") as exc_info: await authenticated_client.post("/conversations", json={}) + assert exc_info.value.status_code == EveApiResponse.BAD_REQUEST.value -async def test_500_raises_server_error(mock_api, authenticated_client: EVEClient): - mock_api.get("/broken").mock(return_value=Response(500, json={ - "detail": "Internal server error", - })) - with pytest.raises(ServerError): +async def test_500_raises_server_error( + mock_api, authenticated_client: EVEClient +): + """Test that 500 response code raises ServerError""" + mock_api.get("/broken").mock( + return_value=Response( + EveApiResponse.INTERNAL_SERVER_ERROR.value, + json={ + "detail": "Internal server error", + }, + ) + ) + + with pytest.raises(ServerError) as exc_info: await authenticated_client.get("/broken") + assert ( + exc_info.value.status_code + == EveApiResponse.INTERNAL_SERVER_ERROR.value + ) + async def test_422_raises_api_error(mock_api, authenticated_client: EVEClient): - mock_api.post("/conversations").mock(return_value=Response(422, json={ - "detail": [{"msg": "field required", "type": "missing"}], - })) + """Test that 422 response code raises APIError""" + random_error = 422 + mock_api.post("/conversations").mock( + return_value=Response( + random_error, + json={ + "detail": [{"msg": "field required", "type": "missing"}], + }, + ) + ) with pytest.raises(APIError) as exc_info: await authenticated_client.post("/conversations", json={}) - assert exc_info.value.status_code == 422 + assert exc_info.value.status_code == random_error # --- Streaming --- @@ -217,18 +329,21 @@ def _sse_response(*events: dict, done: bool = True) -> Response: lines.append("data: [DONE]\n\n") body = "".join(lines) return Response( - 200, + EveApiResponse.SUCCESS.value, content=body.encode(), headers={"content-type": "text/event-stream"}, ) async def test_stream(mock_api, authenticated_client: EVEClient): + """Test streaming""" + status_text = "status" + final_text = "final" events = [ - {"type": "status", "content": "Searching..."}, + {"type": status_text, "content": "Searching..."}, {"type": "token", "content": "Hello"}, {"type": "token", "content": " world"}, - {"type": "final", "content": "Hello world", "message_id": "m-1"}, + {"type": final_text, "content": "Hello world", "message_id": "m-1"}, ] mock_api.post("/conversations/c-1/stream_messages").mock( return_value=_sse_response(*events) @@ -242,15 +357,19 @@ async def test_stream(mock_api, authenticated_client: EVEClient): collected.append(event) # Should stop after "final" event - assert len(collected) == 4 - assert collected[0]["type"] == "status" - assert collected[-1]["type"] == "final" + assert len(collected) == len(events) + assert collected[0]["type"] == status_text + assert collected[-1]["type"] == final_text -async def test_stream_stops_on_error_event(mock_api, authenticated_client: EVEClient): +async def test_stream_stops_on_error_event( + mock_api, authenticated_client: EVEClient +): + """Test that streaming stops on an error event""" + error_text = "error" events = [ {"type": "token", "content": "partial"}, - {"type": "error", "content": "Something went wrong"}, + {"type": error_text, "content": "Something went wrong"}, ] mock_api.post("/conversations/c-1/stream_messages").mock( return_value=_sse_response(*events, done=False) @@ -263,13 +382,16 @@ async def test_stream_stops_on_error_event(mock_api, authenticated_client: EVECl ): collected.append(event) - assert len(collected) == 2 - assert collected[-1]["type"] == "error" + assert len(collected) == len(events) + assert collected[-1]["type"] == error_text async def test_stream_error_status(mock_api, authenticated_client: EVEClient): + """Test status from stream error""" mock_api.post("/conversations/c-1/stream_messages").mock( - return_value=Response(401, json={"detail": "Unauthorized"}) + return_value=Response( + EveApiResponse.INVALID_CREDS.value, json={"detail": "Unauthorized"} + ) ) with pytest.raises(APIError): @@ -284,16 +406,18 @@ async def test_stream_error_status(mock_api, authenticated_client: EVEClient): async def test_context_manager(base_url: str): + """Test the EVEClient as a context manager""" async with EVEClient(base_url) as eve: - assert eve._http is not None + assert eve._http is not None # pylint:disable=protected-access - assert eve._http is None + assert eve._http is None # pylint:disable=protected-access async def test_close(base_url: str): + """Test closing the EVEClient""" eve = EVEClient(base_url) - await eve._ensure_http_client() - assert eve._http is not None + await eve._ensure_http_client() # pylint:disable=protected-access + assert eve._http is not None # pylint:disable=protected-access await eve.close() - assert eve._http is None + assert eve._http is None # pylint:disable=protected-access From 2590a7370995f95dc6954d8dd5f993c407181333 Mon Sep 17 00:00:00 2001 From: r-spiewak <63987228+r-spiewak@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:26:23 -0400 Subject: [PATCH 04/25] feat: Cover client.py --- tests/test_client.py | 72 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/tests/test_client.py b/tests/test_client.py index 0ef63f4..b1e75a4 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -5,7 +5,7 @@ import pytest from httpx import Response -from eve_api import ( # StreamError, +from eve_api import ( APIError, AuthenticationError, EveApiResponse, @@ -14,6 +14,7 @@ NotAuthenticatedError, NotFoundError, ServerError, + StreamError, ValidationError, ) @@ -317,6 +318,24 @@ async def test_422_raises_api_error(mock_api, authenticated_client: EVEClient): assert exc_info.value.status_code == random_error +async def test_response_invalid_json_raises_api_error( + mock_api, authenticated_client: EVEClient +): + """Test that invalid JSON in the response raises APIError""" + random_error = 452 + mock_api.post("/conversations").mock( + return_value=Response( + random_error, + json="Some invalid JSON", + ) + ) + + with pytest.raises(APIError) as exc_info: + await authenticated_client.post("/conversations", json={}) + + assert exc_info.value.status_code == random_error + + # --- Streaming --- @@ -402,6 +421,57 @@ async def test_stream_error_status(mock_api, authenticated_client: EVEClient): pass +async def test_stream_done_no_error(mock_api, authenticated_client: EVEClient): + """Test streaming when it returns data: [DONE] with no errors""" + events = [ + {"type": "token", "content": "Hello"}, + {"type": "token", "content": " world"}, + ] + mock_api.post("/conversations/c-1/stream_messages").mock( + return_value=_sse_response(*events) + ) + + collected = [] + async for event in authenticated_client.stream( + "/conversations/c-1/stream_messages", + json={"query": "Hello"}, + ): + collected.append(event) + + # Should stop even if no "final" event + assert len(collected) == len(events) + + +async def test_stream_error_invalid_json( + mock_api, authenticated_client: EVEClient +): + """Test status from stream error""" + events = [ + {"type": "token", "content": "Hello"}, + {"type": "token", "content": " world"}, + ] + lines = [] + for event in events: + lines.append(f"data: {json.dumps(event)}\n\n") + lines.append("data: Some invalid JSON") + body = "".join(lines) + response = Response( + EveApiResponse.SUCCESS.value, + content=body.encode(), + headers={"content-type": "text/event-stream"}, + ) + mock_api.post("/conversations/c-1/stream_messages").mock( + return_value=response + ) + + with pytest.raises(StreamError): + async for _ in authenticated_client.stream( + "/conversations/c-1/stream_messages", + json={"query": "test"}, + ): + pass + + # --- Context manager --- From 68ec4490c694cce72512ae32054807c73f8fce8a Mon Sep 17 00:00:00 2001 From: r-spiewak <63987228+r-spiewak@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:37:07 -0400 Subject: [PATCH 05/25] feat: Add more response codes and fix names --- src/eve_api/client.py | 2 +- src/eve_api/response.py | 4 +++- tests/test_client.py | 16 +++++++++------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/eve_api/client.py b/src/eve_api/client.py index 09adaa4..fcbd999 100644 --- a/src/eve_api/client.py +++ b/src/eve_api/client.py @@ -212,7 +212,7 @@ async def delete( response = await self.request( "DELETE", path, params=params, timeout=timeout ) - if response.status_code == EveApiResponse.SUCCESS_NO_RESPONSE.value: + if response.status_code == EveApiResponse.SUCCESS_NO_CONTENT.value: return None return response.json() diff --git a/src/eve_api/response.py b/src/eve_api/response.py index 9a610b6..baa4eb8 100644 --- a/src/eve_api/response.py +++ b/src/eve_api/response.py @@ -7,7 +7,9 @@ class EveApiResponse(Enum): """Response code from EVE API.""" SUCCESS = 200 - SUCCESS_NO_RESPONSE = 204 + SUCCESS_CREATED = 201 + SUCCESS_ACCEPTED = 202 + SUCCESS_NO_CONTENT = 204 BAD_REQUEST = 400 INVALID_CREDS = 401 FORBIDDEN = 403 diff --git a/tests/test_client.py b/tests/test_client.py index b1e75a4..b532183 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -26,7 +26,7 @@ async def test_login_success(mock_api, client: EVEClient): tok = "tok-123" mock_api.post("/login").mock( return_value=Response( - 200, + EveApiResponse.SUCCESS.value, json={ "access_token": tok, "refresh_token": "ref-456", @@ -45,7 +45,7 @@ async def test_login_invalid_credentials(mock_api, client: EVEClient): """Test login for invalid credentials raises AuthenticationError""" mock_api.post("/login").mock( return_value=Response( - 401, + EveApiResponse.INVALID_CREDS.value, json={ "detail": "Invalid credentials", }, @@ -62,7 +62,7 @@ async def test_login_account_not_activated(mock_api, client: EVEClient): """Test login for non-activated account raises AuthenticationError""" mock_api.post("/login").mock( return_value=Response( - 403, + EveApiResponse.FORBIDDEN.value, json={ "detail": "Account not activated", }, @@ -93,7 +93,7 @@ async def test_get(mock_api, authenticated_client: EVEClient): user_email = "test@example.com" mock_api.get("/users/me").mock( return_value=Response( - 200, + EveApiResponse.SUCCESS.value, json={ "id": user_id, "email": user_email, @@ -112,7 +112,7 @@ async def test_get_with_params(mock_api, authenticated_client: EVEClient): data_name = "Public" mock_api.get("/collections/public").mock( return_value=Response( - 200, + EveApiResponse.SUCCESS.value, json={ "data": [{"id": "c-1", "name": data_name}], "meta": {"total_count": 1}, @@ -137,7 +137,7 @@ async def test_post(mock_api, authenticated_client: EVEClient): chat_name = "New Chat" mock_api.post("/conversations").mock( return_value=Response( - 201, + EveApiResponse.SUCCESS_CREATED.value, json={ "id": chat_id, "name": "New Chat", @@ -181,7 +181,9 @@ async def test_patch(mock_api, authenticated_client: EVEClient): async def test_delete(mock_api, authenticated_client: EVEClient): """Test deleting a conversation works when a response body is not included""" - mock_api.delete("/conversations/conv-1").mock(return_value=Response(204)) + mock_api.delete("/conversations/conv-1").mock( + return_value=Response(EveApiResponse.SUCCESS_NO_CONTENT.value) + ) result = await authenticated_client.delete("/conversations/conv-1") From 1f8de11795beff29597416a1ce2dde2a7ed66fa5 Mon Sep 17 00:00:00 2001 From: r-spiewak <63987228+r-spiewak@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:48:32 -0400 Subject: [PATCH 06/25] feat: Add new response code, add tests for auth --- src/eve_api/response.py | 1 + tests/conftest.py | 4 +- tests/test_auth.py | 503 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 506 insertions(+), 2 deletions(-) create mode 100644 tests/test_auth.py diff --git a/src/eve_api/response.py b/src/eve_api/response.py index baa4eb8..b46a806 100644 --- a/src/eve_api/response.py +++ b/src/eve_api/response.py @@ -15,3 +15,4 @@ class EveApiResponse(Enum): FORBIDDEN = 403 NOT_FOUND = 404 INTERNAL_SERVER_ERROR = 500 + SERVICE_UNAVAILABLE = 503 diff --git a/tests/conftest.py b/tests/conftest.py index 809b878..fecbc78 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ import respx from httpx import Response -from eve_api import EVEClient +from eve_api import EveApiResponse, EVEClient @pytest.fixture @@ -34,7 +34,7 @@ async def authenticated_client( """Create an authenticated EVE client for testing.""" mock_api.post("/login").mock( return_value=Response( - 200, + EveApiResponse.SUCCESS.value, json={ "access_token": "test-access-token", "refresh_token": "test-refresh-token", diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..90e2f72 --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,503 @@ +"""Tests for the EVEAuth.""" + +from datetime import datetime, timedelta, timezone +from unittest.mock import AsyncMock, MagicMock, patch + +import httpx +import pytest + +from eve_api.auth import ( + AuthenticationError, + EveApiResponse, + EVEAuth, + NotAuthenticatedError, + TokenExpiredError, +) + +# --------------------------------------------------------------------------- +# Helpers / Fixtures +# --------------------------------------------------------------------------- + + +def make_auth(*, access_token=None, refresh_token=None, http_client=None): + """Return an Auth instance with controllable initial state.""" + auth = EVEAuth.__new__(EVEAuth) + auth.access_token = access_token + auth.refresh_token = refresh_token + auth._token_expiry = None # pylint: disable=protected-access + auth._http_client = http_client # pylint: disable=protected-access + # Reproduce whatever base_url the real __init__ would set; adjust if needed. + auth.base_url = "https://example.com" + return auth + + +def make_response(status_code: int, json_data=None, text=""): + """Build a minimal httpx.Response-like mock.""" + response = MagicMock(spec=httpx.Response) + response.status_code = status_code + response.text = text + response.json.return_value = json_data if json_data is not None else {} + return response + + +# --------------------------------------------------------------------------- +# Auth._get_client +# --------------------------------------------------------------------------- + + +class TestGetClient: + """Tests for Auth._get_client.""" + + @pytest.mark.asyncio + @staticmethod + async def test_returns_existing_client_when_set(): + """When _http_client is already set, it is returned directly.""" + existing = MagicMock(spec=httpx.AsyncClient) + auth = make_auth(http_client=existing) + result = await auth._get_client() # pylint: disable=protected-access + assert result is existing + + @pytest.mark.asyncio + @staticmethod + async def test_creates_temporary_client_when_none(): + """When _http_client is None a fresh AsyncClient is created and returned.""" + auth = make_auth(http_client=None) + mock_instance = MagicMock(spec=httpx.AsyncClient) + with patch("eve_api.auth.httpx.AsyncClient") as mock_client: + mock_client.return_value = mock_instance + result = ( + await auth._get_client() # pylint: disable=protected-access + ) + mock_client.assert_called_once_with( + base_url=auth.base_url, timeout=30.0 + ) + assert result is mock_instance + + +# --------------------------------------------------------------------------- +# Auth.login +# --------------------------------------------------------------------------- + + +class TestLogin: + """Tests for Auth.login.""" + + @pytest.mark.asyncio + @staticmethod + async def test_login_unexpected_status_calls_handle_error(): + """A non-success, non-401, non-403 status code triggers _handle_error_response.""" + auth = make_auth() + response = make_response(EveApiResponse.INTERNAL_SERVER_ERROR.value) + + mock_client = AsyncMock(spec=httpx.AsyncClient) + mock_client.post.return_value = response + + with ( + patch.object(auth, "_get_client", return_value=mock_client), + patch.object( + auth, + "_handle_error_response", + side_effect=AuthenticationError("err"), + ) as mock_handle, + ): + with pytest.raises(AuthenticationError, match="err"): + await auth.login("user@example.com", "pw") + + mock_handle.assert_called_once_with(response) + + @pytest.mark.asyncio + @staticmethod + async def test_login_closes_temporary_client_on_success(): + """When no persistent client exists the temporary client is closed after login.""" + auth = make_auth( + http_client=None + ) # no persistent client → should_close = True + response = make_response( + EveApiResponse.SUCCESS.value, + json_data={"access_token": "tok", "refresh_token": "ref"}, + ) + + mock_client = AsyncMock(spec=httpx.AsyncClient) + mock_client.post.return_value = response + + with patch.object(auth, "_get_client", return_value=mock_client): + await auth.login("user@example.com", "pw") + + mock_client.aclose.assert_awaited_once() + + @pytest.mark.asyncio + @staticmethod + async def test_login_closes_temporary_client_on_error(): + """The temporary client is closed even when login raises an exception.""" + auth = make_auth(http_client=None) + response = make_response(EveApiResponse.INVALID_CREDS.value) + + mock_client = AsyncMock(spec=httpx.AsyncClient) + mock_client.post.return_value = response + + with patch.object(auth, "_get_client", return_value=mock_client): + with pytest.raises(AuthenticationError): + await auth.login("bad@example.com", "bad") + + mock_client.aclose.assert_awaited_once() + + @pytest.mark.asyncio + @staticmethod + async def test_login_does_not_close_persistent_client(): + """When a persistent client is supplied it is NOT closed after login.""" + persistent = AsyncMock(spec=httpx.AsyncClient) + auth = make_auth(http_client=persistent) + response = make_response( + EveApiResponse.SUCCESS.value, + json_data={"access_token": "tok", "refresh_token": "ref"}, + ) + persistent.post.return_value = response + + await auth.login("user@example.com", "pw") + + persistent.aclose.assert_not_awaited() + + +# --------------------------------------------------------------------------- +# Auth.refresh +# --------------------------------------------------------------------------- + + +class TestRefresh: + """Full coverage for Auth.refresh.""" + + @pytest.mark.asyncio + @staticmethod + async def test_refresh_raises_when_no_refresh_token(): + """Test that refresh method raises NotAuthenticatedError + when no refresh token available.""" + auth = make_auth() + with pytest.raises( + NotAuthenticatedError, match="No refresh token available" + ): + await auth.refresh() + + @pytest.mark.asyncio + @staticmethod + async def test_refresh_clears_tokens_on_expired_refresh_token(): + """Test that refresh clears token on expired refresh token.""" + auth = make_auth( + access_token="old_access", refresh_token="expired_ref" + ) + response = make_response(EveApiResponse.INVALID_CREDS.value) + + mock_client = AsyncMock(spec=httpx.AsyncClient) + mock_client.post.return_value = response + + with patch.object(auth, "_get_client", return_value=mock_client): + with pytest.raises( + TokenExpiredError, match="Refresh token expired" + ): + await auth.refresh() + + assert auth.access_token is None + assert auth.refresh_token is None + assert auth._token_expiry is None # pylint: disable=protected-access + + @pytest.mark.asyncio + @staticmethod + async def test_refresh_unexpected_status_calls_handle_error(): + """Test that refresh on unexpected status calls _handle_error.""" + auth = make_auth(refresh_token="ref") + response = make_response(500) + + mock_client = AsyncMock(spec=httpx.AsyncClient) + mock_client.post.return_value = response + + with ( + patch.object(auth, "_get_client", return_value=mock_client), + patch.object( + auth, + "_handle_error_response", + side_effect=AuthenticationError("boom"), + ) as mock_handle, + ): + with pytest.raises(AuthenticationError, match="boom"): + await auth.refresh() + + mock_handle.assert_called_once_with(response) + + @pytest.mark.asyncio + @staticmethod + async def test_refresh_updates_access_token_and_expiry_on_success(): + """Test that refresh updates access token and expiry on success.""" + auth = make_auth(refresh_token="ref") + tok = "new_tok" + response = make_response( + EveApiResponse.SUCCESS.value, + json_data={"access_token": tok}, + ) + + mock_client = AsyncMock(spec=httpx.AsyncClient) + mock_client.post.return_value = response + + before = datetime.now(timezone.utc) + with patch.object(auth, "_get_client", return_value=mock_client): + await auth.refresh() + after = datetime.now(timezone.utc) + + assert auth.access_token == tok + assert ( + auth._token_expiry is not None # pylint: disable=protected-access + ) + assert ( + before + < auth._token_expiry # pylint: disable=protected-access + <= after + + EVEAuth._DEFAULT_EXPIRY # pylint: disable=protected-access + ) + + @pytest.mark.asyncio + @staticmethod + async def test_refresh_closes_temporary_client(): + """Test refresh closes temporary client.""" + auth = make_auth(http_client=None, refresh_token="ref") + response = make_response( + EveApiResponse.SUCCESS.value, + json_data={"access_token": "new_tok"}, + ) + + mock_client = AsyncMock(spec=httpx.AsyncClient) + mock_client.post.return_value = response + + with patch.object(auth, "_get_client", return_value=mock_client): + await auth.refresh() + + mock_client.aclose.assert_awaited_once() + + @pytest.mark.asyncio + @staticmethod + async def test_refresh_closes_temporary_client_on_error(): + """Test refresh closes temporary client on error.""" + auth = make_auth(http_client=None, refresh_token="ref") + response = make_response(EveApiResponse.INVALID_CREDS.value) + + mock_client = AsyncMock(spec=httpx.AsyncClient) + mock_client.post.return_value = response + + with patch.object(auth, "_get_client", return_value=mock_client): + with pytest.raises(TokenExpiredError): + await auth.refresh() + + mock_client.aclose.assert_awaited_once() + + +# --------------------------------------------------------------------------- +# Auth.get_headers +# --------------------------------------------------------------------------- + + +class TestGetHeaders: + """Tests for Auth.get_headers.""" + + @staticmethod + def test_raises_when_not_authenticated(): + """Raises NotAuthenticatedError when access_token is None.""" + auth = make_auth() + with pytest.raises(NotAuthenticatedError, match="Not authenticated"): + auth.get_headers() + + @staticmethod + def test_returns_bearer_header_when_authenticated(): + """Test GET returns bearer token when authenticated.""" + auth = make_auth(access_token="mytoken") + assert auth.get_headers() == {"Authorization": "Bearer mytoken"} + + +# --------------------------------------------------------------------------- +# Auth.ensure_authenticated +# --------------------------------------------------------------------------- + + +class TestEnsureAuthenticated: + """Tests for Auth.ensure_authenticated.""" + + @pytest.mark.asyncio + @staticmethod + async def test_calls_refresh_when_token_should_be_refreshed(): + """When _should_refresh() returns True, refresh() is awaited.""" + auth = make_auth(access_token="tok") + + with ( + patch.object(auth, "_should_refresh", return_value=True), + patch.object( + auth, "refresh", new_callable=AsyncMock + ) as mock_refresh, + ): + await auth.ensure_authenticated() + + mock_refresh.assert_awaited_once() + + @pytest.mark.asyncio + @staticmethod + async def test_does_not_refresh_when_token_is_fresh(): + """Test that it does not refresh when token is fresh.""" + auth = make_auth(access_token="tok") + + with ( + patch.object(auth, "_should_refresh", return_value=False), + patch.object( + auth, "refresh", new_callable=AsyncMock + ) as mock_refresh, + ): + await auth.ensure_authenticated() + + mock_refresh.assert_not_awaited() + + @pytest.mark.asyncio + @staticmethod + async def test_raises_when_no_access_token(): + """Test that NotAuthenticatedError is raised when no + access token is available.""" + auth = make_auth() + with pytest.raises(NotAuthenticatedError): + await auth.ensure_authenticated() + + +# --------------------------------------------------------------------------- +# Auth._should_refresh +# --------------------------------------------------------------------------- + + +class TestShouldRefresh: + """Tests for Auth._should_refresh, including the no-expiry branch.""" + + @staticmethod + def test_returns_false_when_no_token_expiry(): + """When _token_expiry is None the method returns False without error.""" + auth = make_auth() + assert auth._token_expiry is None # pylint: disable=protected-access + assert ( + auth._should_refresh() is False # pylint: disable=protected-access + ) + + @staticmethod + def test_returns_true_when_expiry_is_past(): + """Test that _should_refresh returns True when token is expired.""" + auth = make_auth() + auth._token_expiry = datetime.now( # pylint: disable=protected-access + timezone.utc + ) - timedelta(seconds=1) + assert ( + auth._should_refresh() is True # pylint: disable=protected-access + ) + + @staticmethod + def test_returns_false_when_expiry_is_far_future(): + """Test that _should_refresh returns False when expiry is + far into the future.""" + auth = make_auth() + auth._token_expiry = datetime.now( # pylint: disable=protected-access + timezone.utc + ) + timedelta(hours=1) + assert ( + auth._should_refresh() is False # pylint: disable=protected-access + ) + + @staticmethod + def test_returns_true_within_refresh_buffer(): + """Token inside the REFRESH_BUFFER window should trigger a refresh.""" + auth = make_auth() + # Set expiry just inside the buffer so now >= expiry - buffer + auth._token_expiry = ( # pylint: disable=protected-access + datetime.now(timezone.utc) + + EVEAuth._REFRESH_BUFFER # pylint: disable=protected-access + - timedelta(seconds=1) + ) + assert ( + auth._should_refresh() is True # pylint: disable=protected-access + ) + + +# --------------------------------------------------------------------------- +# Auth._handle_error_response +# --------------------------------------------------------------------------- + + +class TestHandleErrorResponse: + """Tests for the static _handle_error_response method.""" + + @staticmethod + def test_raises_with_detail_from_json(): + """Test _handle_error_response raises AuthenticationError with details.""" + response = make_response( + EveApiResponse.BAD_REQUEST.value, + json_data={"detail": "Bad thing happened"}, + ) + with pytest.raises(AuthenticationError, match="Bad thing happened"): + EVEAuth._handle_error_response( # pylint: disable=protected-access + response + ) + + @staticmethod + def test_raises_with_stringified_json_when_no_detail_key(): + """Test _handle_error_response raises AuthenticationError when no detail key.""" + response = make_response( + EveApiResponse.BAD_REQUEST.value, json_data={"error": "nope"} + ) + with pytest.raises(AuthenticationError, match="Authentication failed"): + EVEAuth._handle_error_response( # pylint: disable=protected-access + response + ) + + @staticmethod + def test_falls_back_to_response_text_when_json_fails(): + """Test fallback to response text when JSON fails.""" + response = MagicMock(spec=httpx.Response) + response.status_code = EveApiResponse.SERVICE_UNAVAILABLE.value + response.text = "Service Unavailable" + response.json.side_effect = ValueError("not json") + with pytest.raises(AuthenticationError, match="Service Unavailable"): + EVEAuth._handle_error_response( # pylint: disable=protected-access + response + ) + + @staticmethod + def test_falls_back_to_status_code_when_text_is_empty(): + """Test fallback to status code when text is empty.""" + response = MagicMock(spec=httpx.Response) + response.status_code = EveApiResponse.SERVICE_UNAVAILABLE.value + response.text = "" + response.json.side_effect = ValueError("not json") + with pytest.raises( + AuthenticationError, + match=f"HTTP {EveApiResponse.SERVICE_UNAVAILABLE.value}", + ): + EVEAuth._handle_error_response( # pylint: disable=protected-access + response + ) + + +# --------------------------------------------------------------------------- +# Auth.clear +# --------------------------------------------------------------------------- + + +class TestClear: + """Tests for Auth.clear.""" + + @staticmethod + def test_clear_resets_all_tokens(): + """Test that clear resets all tokens.""" + auth = make_auth(access_token="tok", refresh_token="ref") + auth._token_expiry = datetime.now( # pylint: disable=protected-access + timezone.utc + ) + + auth.clear() + + assert auth.access_token is None + assert auth.refresh_token is None + assert auth._token_expiry is None # pylint: disable=protected-access + + @staticmethod + def test_clear_is_idempotent(): + """Calling clear() on an already-cleared instance does not raise.""" + auth = make_auth() + auth.clear() # should not raise + assert auth.access_token is None From f2c4fa3d34f80729c81581f5691fa977b292ee87 Mon Sep 17 00:00:00 2001 From: William Fawcett Date: Tue, 7 Apr 2026 16:12:35 +0100 Subject: [PATCH 07/25] Remove referecnes to STARS --- .pylintrc | 2 -- 1 file changed, 2 deletions(-) diff --git a/.pylintrc b/.pylintrc index 538af09..aca99e6 100644 --- a/.pylintrc +++ b/.pylintrc @@ -445,8 +445,6 @@ disable=raw-checker-failed, enable= per-file-ignores = - src/stars/mcp_servers/python_toolbox/**:duplicate-code, - src/stars/docs/conf.py:invalid-name [METHOD_ARGS] From 45b5ffe1ee1edae81ca0a9b14d8068eec049c797 Mon Sep 17 00:00:00 2001 From: William Fawcett Date: Tue, 7 Apr 2026 16:15:03 +0100 Subject: [PATCH 08/25] clearing output --- examples/tutorial.ipynb | 231 ++++------------------------------------ 1 file changed, 21 insertions(+), 210 deletions(-) diff --git a/examples/tutorial.ipynb b/examples/tutorial.ipynb index 9e39416..129a8f0 100644 --- a/examples/tutorial.ipynb +++ b/examples/tutorial.ipynb @@ -27,15 +27,7 @@ "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Authenticated: True\n" - ] - } - ], + "outputs": [], "source": [ "from eve_api import EVEClient\n", "\n", @@ -57,18 +49,9 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "User: james@trillium.tech\n", - "Name: None None\n" - ] - } - ], + "outputs": [], "source": [ "me = await eve.get(\"/users/me\")\n", "print(f\"User: {me['email']}\")\n", @@ -77,22 +60,9 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " - Wiley AI Gateway\n", - " - Wikipedia EO\n", - " - EVE open access\n", - " - ESA EO Knowledge Base\n", - "\n", - "Total: 4\n" - ] - } - ], + "outputs": [], "source": [ "collections = await eve.get(\"/collections/public\")\n", "\n", @@ -111,17 +81,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Created: 698f4c7a37c7389604facd9f — Tutorial Chat\n" - ] - } - ], + "outputs": [], "source": [ "# Create a conversation\n", "conv = await eve.post(\"/conversations\", json={\"name\": \"Tutorial Chat\"})\n", @@ -131,17 +93,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Renamed to: Renamed Chat\n" - ] - } - ], + "outputs": [], "source": [ "# Rename it\n", "updated = await eve.patch(f\"/conversations/{conv_id}\", json={\"name\": \"Renamed Chat\"})\n", @@ -150,17 +104,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Deleted.\n" - ] - } - ], + "outputs": [], "source": [ "# Delete it\n", "await eve.delete(f\"/conversations/{conv_id}\")\n", @@ -178,111 +124,9 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "### **Synthetic Aperture Radar (SAR) – Definition & Overview**\n", - "\n", - "**Synthetic Aperture Radar (SAR)** is a **remote sensing technology** that uses **microwave signals** to create high-resolution images of Earth's surface, regardless of weather conditions (clouds, rain, darkness). Unlike optical sensors (e.g., cameras), SAR actively emits and receives radar signals to map terrain, monitor changes, and detect objects.\n", - "\n", - "---\n", - "\n", - "### **Key Aspects of SAR**\n", - "1. **Active Sensing**:\n", - " - SAR systems **emit microwave pulses** toward the Earth and **measure the reflected signals** (backscatter).\n", - " - This allows imaging **day or night**, unlike passive optical sensors that rely on sunlight.\n", - "\n", - "2. **Synthetic Aperture Principle**:\n", - " - A **moving radar antenna** (e.g., on a satellite or aircraft) simulates a much larger antenna by combining signals from multiple positions along its flight path.\n", - " - This **increases resolution** significantly compared to traditional radar.\n", - "\n", - "3. **Wavelength Bands**:\n", - " - SAR operates in **microwave frequencies** (e.g., **X-band, C-band, L-band, P-band**), each with different penetration and sensitivity properties:\n", - " - **X-band (~3 cm)**: High resolution, used for detailed surface mapping (e.g., urban areas, ships).\n", - " - **C-band (~5.6 cm)**: Balanced resolution and penetration (e.g., agriculture, ice monitoring).\n", - " - **L-band (~24 cm)**: Deeper penetration (e.g., forest biomass, subsurface features).\n", - " - **P-band (~68 cm)**: Very deep penetration (e.g., underground structures, dense vegetation).\n", - "\n", - "4. **Polarization**:\n", - " - SAR can transmit and receive signals in **horizontal (H) or vertical (V) polarizations**, enabling different surface property analyses (e.g., HH, HV, VV, VH).\n", - "\n", - "5. **All-Weather Capability**:\n", - " - Microwaves **penetrate clouds and rain**, making SAR ideal for **disaster monitoring** (floods, earthquakes) and **polar regions** (ice tracking).\n", - "\n", - "---\n", - "\n", - "### **How SAR Works (Simplified)**\n", - "1. **Signal Transmission**: The radar antenna emits a microwave pulse toward the target area.\n", - "2. **Backscatter Reception**: The signal reflects off the surface (or objects) and returns to the antenna.\n", - "3. **Data Processing**: The system combines multiple returned signals (from different positions) to **synthesize a large aperture**, improving resolution.\n", - "4. **Image Formation**: The processed data generates a **2D or 3D image** representing surface properties (e.g., roughness, moisture, structure).\n", - "\n", - "---\n", - "\n", - "### **Applications of SAR**\n", - "| **Field** | **Key Applications** |\n", - "|-------------------------|-------------------------------------------------------------------------------------|\n", - "| **Environmental Monitoring** | Deforestation, glacier/ice sheet tracking, oil spill detection, wetland mapping. |\n", - "| **Disaster Management** | Flood/earthquake damage assessment, landslide detection, volcanic activity monitoring. |\n", - "| **Agriculture** | Crop health, soil moisture, harvest forecasting. |\n", - "| **Urban Planning** | Infrastructure monitoring, subsidence detection, urban sprawl analysis. |\n", - "| **Defense & Security** | Ship/vehicle detection, terrain analysis, surveillance. |\n", - "| **Oceanography** | Wave/wind patterns, ship tracking, coastal erosion. |\n", - "| **Geology** | Mineral exploration, land deformation (e.g., from mining or earthquakes). |\n", - "\n", - "---\n", - "\n", - "### **Advantages of SAR**\n", - "✅ **Weather-independent**: Works through clouds, rain, and darkness.\n", - "✅ **High resolution**: Can detect objects as small as **a few meters** (e.g., cars, ships).\n", - "✅ **3D Capability**: **Interferometric SAR (InSAR)** measures elevation changes (e.g., land subsidence, volcanic inflation).\n", - "✅ **Penetration**: Some wavelengths (L-band, P-band) can **see through vegetation or dry soil**.\n", - "✅ **Global Coverage**: Satellites like **ESA’s Sentinel-1** provide **systematic, worldwide data**.\n", - "\n", - "---\n", - "\n", - "### **Limitations of SAR**\n", - "❌ **Complex Data Processing**: Requires advanced algorithms for noise reduction and interpretation.\n", - "❌ **Speckle Noise**: Random variations in signal can degrade image quality (mitigated via multi-look processing).\n", - "❌ **Limited Spectral Information**: Unlike optical sensors, SAR does not provide color or multispectral data.\n", - "❌ **Cost & Size**: High-resolution SAR systems (e.g., on satellites) are expensive and require large antennas.\n", - "\n", - "---\n", - "\n", - "### **Key SAR Missions & Satellites**\n", - "| **Mission/Satellite** | **Agency** | **Band** | **Key Features** |\n", - "|-----------------------------|------------------|----------|---------------------------------------------------------------------------------|\n", - "| **Sentinel-1 (A/B)** | ESA (Copernicus) | C-band | Global coverage, free/open data, disaster monitoring. |\n", - "| **TerraSAR-X / TanDEM-X** | DLR (Germany) | X-band | High-resolution (1–16 m), 3D elevation mapping. |\n", - "| **RADARSAT-2** | CSA (Canada) | C-band | Polar monitoring, maritime surveillance. |\n", - "| **ALOS-2 (PALSAR-2)** | JAXA (Japan) | L-band | Forest biomass, disaster response. |\n", - "| **SAOCOM** | CONAE (Argentina)| L-band | Soil moisture, agriculture, emergency management. |\n", - "| **NISAR** (Upcoming, 2024) | NASA/ISRO | L & S-band| First dual-frequency SAR for Earth deformation studies. |\n", - "\n", - "---\n", - "\n", - "### **Why is SAR Important?**\n", - "- **Climate Change Studies**: Tracks ice melt, sea-level rise, and deforestation.\n", - "- **Disaster Response**: Provides rapid damage assessments (e.g., after hurricanes or earthquakes).\n", - "- **Food Security**: Monitors crop health and predicts droughts.\n", - "- **Infrastructure Safety**: Detects land subsidence near cities or pipelines.\n", - "- **Defense & Maritime**: Identifies illegal fishing or ship movements in remote areas.\n", - "\n", - "---\n", - "### **Further Reading (If Available)**\n", - "For hands-on exploration, ESA provides **free SAR data** via:\n", - "- [Copernicus Open Access Hub](https://scihub.copernicus.eu/) (Sentinel-1)\n", - "- [ESA’s Sentinel Online](https://sentinels.copernicus.eu/)\n", - "- [NASA’s ASF DAAC](https://asf.alaska.edu/) (for ALOS, RADARSAT, etc.)\n", - "\n", - "Would you like a deeper dive into **InSAR (Interferometric SAR)** or a specific application (e.g., flood mapping)?\n" - ] - } - ], + "outputs": [], "source": [ "conv = await eve.post(\"/conversations\", json={\"name\": \"Stream Demo\"})\n", "conv_id = conv[\"id\"]\n", @@ -305,17 +149,9 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Conversation deleted.\n" - ] - } - ], + "outputs": [], "source": [ "# Clean up\n", "await eve.delete(f\"/conversations/{conv_id}\")\n", @@ -333,18 +169,9 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Status: 200\n", - "{'status': 'healthy'}\n" - ] - } - ], + "outputs": [], "source": [ "resp = await eve.request(\"GET\", \"/health\", auth_required=False)\n", "print(f\"Status: {resp.status_code}\")\n", @@ -381,17 +208,9 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Not found (404)\n" - ] - } - ], + "outputs": [], "source": [ "from eve_api import NotFoundError, ForbiddenError, APIError\n", "\n", @@ -414,17 +233,9 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Connection closed.\n" - ] - } - ], + "outputs": [], "source": [ "await eve.close()\n", "print(\"Connection closed.\")" From 383e96c191db7b3053ab2143423e34b5612e6248 Mon Sep 17 00:00:00 2001 From: William Fawcett Date: Tue, 7 Apr 2026 16:15:40 +0100 Subject: [PATCH 09/25] cleanup --- examples/tutorial.ipynb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/examples/tutorial.ipynb b/examples/tutorial.ipynb index 129a8f0..e424467 100644 --- a/examples/tutorial.ipynb +++ b/examples/tutorial.ipynb @@ -240,13 +240,6 @@ "await eve.close()\n", "print(\"Connection closed.\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From 91f46759ce194097fc11a55d6db06245493c89c0 Mon Sep 17 00:00:00 2001 From: William Fawcett Date: Wed, 8 Apr 2026 10:50:11 +0100 Subject: [PATCH 10/25] Remove custom EveApiResponse in favour of HTTPStatus --- src/eve_api/__init__.py | 3 --- src/eve_api/auth.py | 13 ++++++----- src/eve_api/client.py | 17 +++++++------- src/eve_api/exceptions.py | 10 ++++----- src/eve_api/response.py | 18 --------------- tests/conftest.py | 6 +++-- tests/test_auth.py | 29 ++++++++++++------------ tests/test_client.py | 47 ++++++++++++++++++++------------------- 8 files changed, 64 insertions(+), 79 deletions(-) delete mode 100644 src/eve_api/response.py diff --git a/src/eve_api/__init__.py b/src/eve_api/__init__.py index 748d649..f20a558 100644 --- a/src/eve_api/__init__.py +++ b/src/eve_api/__init__.py @@ -14,15 +14,12 @@ TokenExpiredError, ValidationError, ) -from .response import EveApiResponse - __all__ = [ "__version__", "EVEClient", "EVEError", "APIError", "AuthenticationError", - "EveApiResponse", "ForbiddenError", "NotAuthenticatedError", "NotFoundError", diff --git a/src/eve_api/auth.py b/src/eve_api/auth.py index 534d658..f4fe6a5 100644 --- a/src/eve_api/auth.py +++ b/src/eve_api/auth.py @@ -7,12 +7,13 @@ import httpx +from http import HTTPStatus + from .exceptions import ( AuthenticationError, NotAuthenticatedError, TokenExpiredError, ) -from .response import EveApiResponse class EVEAuth: @@ -86,11 +87,11 @@ async def login(self, email: str, password: str) -> None: json={"email": email, "password": password}, ) - if response.status_code == EveApiResponse.INVALID_CREDS.value: + if response.status_code == HTTPStatus.UNAUTHORIZED: raise AuthenticationError("Invalid email or password") - if response.status_code == EveApiResponse.FORBIDDEN.value: + if response.status_code == HTTPStatus.FORBIDDEN: raise AuthenticationError("Account not activated") - if response.status_code != EveApiResponse.SUCCESS.value: + if response.status_code != HTTPStatus.OK: self._handle_error_response(response) data = response.json() @@ -121,7 +122,7 @@ async def refresh(self) -> None: json={"refresh_token": self.refresh_token}, ) - if response.status_code == EveApiResponse.INVALID_CREDS.value: + if response.status_code == HTTPStatus.UNAUTHORIZED: # Refresh token expired self.access_token = None self.refresh_token = None @@ -129,7 +130,7 @@ async def refresh(self) -> None: raise TokenExpiredError( "Refresh token expired. Please login again." ) - if response.status_code != EveApiResponse.SUCCESS.value: + if response.status_code != HTTPStatus.OK: self._handle_error_response(response) data = response.json() diff --git a/src/eve_api/client.py b/src/eve_api/client.py index fcbd999..bb3af97 100644 --- a/src/eve_api/client.py +++ b/src/eve_api/client.py @@ -9,6 +9,8 @@ import httpx from .auth import EVEAuth +from http import HTTPStatus + from .exceptions import ( APIError, ForbiddenError, @@ -17,7 +19,6 @@ StreamError, ValidationError, ) -from .response import EveApiResponse class EVEClient: @@ -212,7 +213,7 @@ async def delete( response = await self.request( "DELETE", path, params=params, timeout=timeout ) - if response.status_code == EveApiResponse.SUCCESS_NO_CONTENT.value: + if response.status_code == HTTPStatus.NO_CONTENT: return None return response.json() @@ -252,7 +253,7 @@ async def stream( headers=headers, timeout=timeout or 300.0, ) as response: - if response.status_code >= EveApiResponse.BAD_REQUEST.value: + if response.status_code >= HTTPStatus.BAD_REQUEST: await response.aread() self._handle_error(response) @@ -326,7 +327,7 @@ async def request( # pylint: disable=too-many-positional-arguments timeout=timeout or self._timeout, ) - if response.status_code >= EveApiResponse.BAD_REQUEST.value: + if response.status_code >= HTTPStatus.BAD_REQUEST: self._handle_error(response) return response @@ -355,12 +356,12 @@ def _handle_error(response: httpx.Response) -> None: except Exception: # pylint: disable=broad-exception-caught message = response.text or f"HTTP {status}" - if status == EveApiResponse.NOT_FOUND.value: + if status == HTTPStatus.NOT_FOUND: raise NotFoundError("resource", "unknown") - if status == EveApiResponse.FORBIDDEN.value: + if status == HTTPStatus.FORBIDDEN: raise ForbiddenError(message) - if status == EveApiResponse.BAD_REQUEST.value: + if status == HTTPStatus.BAD_REQUEST: raise ValidationError(message) - if status >= EveApiResponse.INTERNAL_SERVER_ERROR.value: + if status >= HTTPStatus.INTERNAL_SERVER_ERROR: raise ServerError(message, status_code=status) raise APIError(message, status_code=status) diff --git a/src/eve_api/exceptions.py b/src/eve_api/exceptions.py index 1d0bc29..d4ea6fe 100644 --- a/src/eve_api/exceptions.py +++ b/src/eve_api/exceptions.py @@ -4,7 +4,7 @@ from typing import Any -from .response import EveApiResponse +from http import HTTPStatus class EVEError(Exception): @@ -68,7 +68,7 @@ def __init__(self, resource: str, resource_id: str) -> None: """ super().__init__( f"{resource.title()} not found: {resource_id}", - status_code=EveApiResponse.NOT_FOUND.value, + status_code=HTTPStatus.NOT_FOUND, details={"resource": resource, "id": resource_id}, ) @@ -82,7 +82,7 @@ def __init__(self, message: str = "Access forbidden") -> None: Args: message: Human-readable error message. """ - super().__init__(message, status_code=EveApiResponse.FORBIDDEN.value) + super().__init__(message, status_code=HTTPStatus.FORBIDDEN) class ValidationError(APIError): @@ -99,7 +99,7 @@ def __init__( """ super().__init__( message, - status_code=EveApiResponse.BAD_REQUEST.value, + status_code=HTTPStatus.BAD_REQUEST, details=details, ) @@ -110,7 +110,7 @@ class ServerError(APIError): def __init__( self, message: str = "Internal server error", - status_code: int = EveApiResponse.INTERNAL_SERVER_ERROR.value, + status_code: int = HTTPStatus.INTERNAL_SERVER_ERROR, details: dict[str, Any] | None = None, ) -> None: """Initialise the server error. diff --git a/src/eve_api/response.py b/src/eve_api/response.py deleted file mode 100644 index b46a806..0000000 --- a/src/eve_api/response.py +++ /dev/null @@ -1,18 +0,0 @@ -"""This module contains an enum for response codes from EVE.""" - -from enum import Enum - - -class EveApiResponse(Enum): - """Response code from EVE API.""" - - SUCCESS = 200 - SUCCESS_CREATED = 201 - SUCCESS_ACCEPTED = 202 - SUCCESS_NO_CONTENT = 204 - BAD_REQUEST = 400 - INVALID_CREDS = 401 - FORBIDDEN = 403 - NOT_FOUND = 404 - INTERNAL_SERVER_ERROR = 500 - SERVICE_UNAVAILABLE = 503 diff --git a/tests/conftest.py b/tests/conftest.py index fecbc78..0fbc73f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,9 @@ import respx from httpx import Response -from eve_api import EveApiResponse, EVEClient +from http import HTTPStatus + +from eve_api import EVEClient @pytest.fixture @@ -34,7 +36,7 @@ async def authenticated_client( """Create an authenticated EVE client for testing.""" mock_api.post("/login").mock( return_value=Response( - EveApiResponse.SUCCESS.value, + HTTPStatus.OK, json={ "access_token": "test-access-token", "refresh_token": "test-refresh-token", diff --git a/tests/test_auth.py b/tests/test_auth.py index 90e2f72..a98524d 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -6,9 +6,10 @@ import httpx import pytest +from http import HTTPStatus + from eve_api.auth import ( AuthenticationError, - EveApiResponse, EVEAuth, NotAuthenticatedError, TokenExpiredError, @@ -87,7 +88,7 @@ class TestLogin: async def test_login_unexpected_status_calls_handle_error(): """A non-success, non-401, non-403 status code triggers _handle_error_response.""" auth = make_auth() - response = make_response(EveApiResponse.INTERNAL_SERVER_ERROR.value) + response = make_response(HTTPStatus.INTERNAL_SERVER_ERROR) mock_client = AsyncMock(spec=httpx.AsyncClient) mock_client.post.return_value = response @@ -113,7 +114,7 @@ async def test_login_closes_temporary_client_on_success(): http_client=None ) # no persistent client → should_close = True response = make_response( - EveApiResponse.SUCCESS.value, + HTTPStatus.OK, json_data={"access_token": "tok", "refresh_token": "ref"}, ) @@ -130,7 +131,7 @@ async def test_login_closes_temporary_client_on_success(): async def test_login_closes_temporary_client_on_error(): """The temporary client is closed even when login raises an exception.""" auth = make_auth(http_client=None) - response = make_response(EveApiResponse.INVALID_CREDS.value) + response = make_response(HTTPStatus.UNAUTHORIZED) mock_client = AsyncMock(spec=httpx.AsyncClient) mock_client.post.return_value = response @@ -148,7 +149,7 @@ async def test_login_does_not_close_persistent_client(): persistent = AsyncMock(spec=httpx.AsyncClient) auth = make_auth(http_client=persistent) response = make_response( - EveApiResponse.SUCCESS.value, + HTTPStatus.OK, json_data={"access_token": "tok", "refresh_token": "ref"}, ) persistent.post.return_value = response @@ -184,7 +185,7 @@ async def test_refresh_clears_tokens_on_expired_refresh_token(): auth = make_auth( access_token="old_access", refresh_token="expired_ref" ) - response = make_response(EveApiResponse.INVALID_CREDS.value) + response = make_response(HTTPStatus.UNAUTHORIZED) mock_client = AsyncMock(spec=httpx.AsyncClient) mock_client.post.return_value = response @@ -229,7 +230,7 @@ async def test_refresh_updates_access_token_and_expiry_on_success(): auth = make_auth(refresh_token="ref") tok = "new_tok" response = make_response( - EveApiResponse.SUCCESS.value, + HTTPStatus.OK, json_data={"access_token": tok}, ) @@ -258,7 +259,7 @@ async def test_refresh_closes_temporary_client(): """Test refresh closes temporary client.""" auth = make_auth(http_client=None, refresh_token="ref") response = make_response( - EveApiResponse.SUCCESS.value, + HTTPStatus.OK, json_data={"access_token": "new_tok"}, ) @@ -275,7 +276,7 @@ async def test_refresh_closes_temporary_client(): async def test_refresh_closes_temporary_client_on_error(): """Test refresh closes temporary client on error.""" auth = make_auth(http_client=None, refresh_token="ref") - response = make_response(EveApiResponse.INVALID_CREDS.value) + response = make_response(HTTPStatus.UNAUTHORIZED) mock_client = AsyncMock(spec=httpx.AsyncClient) mock_client.post.return_value = response @@ -426,7 +427,7 @@ class TestHandleErrorResponse: def test_raises_with_detail_from_json(): """Test _handle_error_response raises AuthenticationError with details.""" response = make_response( - EveApiResponse.BAD_REQUEST.value, + HTTPStatus.BAD_REQUEST, json_data={"detail": "Bad thing happened"}, ) with pytest.raises(AuthenticationError, match="Bad thing happened"): @@ -438,7 +439,7 @@ def test_raises_with_detail_from_json(): def test_raises_with_stringified_json_when_no_detail_key(): """Test _handle_error_response raises AuthenticationError when no detail key.""" response = make_response( - EveApiResponse.BAD_REQUEST.value, json_data={"error": "nope"} + HTTPStatus.BAD_REQUEST, json_data={"error": "nope"} ) with pytest.raises(AuthenticationError, match="Authentication failed"): EVEAuth._handle_error_response( # pylint: disable=protected-access @@ -449,7 +450,7 @@ def test_raises_with_stringified_json_when_no_detail_key(): def test_falls_back_to_response_text_when_json_fails(): """Test fallback to response text when JSON fails.""" response = MagicMock(spec=httpx.Response) - response.status_code = EveApiResponse.SERVICE_UNAVAILABLE.value + response.status_code = HTTPStatus.SERVICE_UNAVAILABLE response.text = "Service Unavailable" response.json.side_effect = ValueError("not json") with pytest.raises(AuthenticationError, match="Service Unavailable"): @@ -461,12 +462,12 @@ def test_falls_back_to_response_text_when_json_fails(): def test_falls_back_to_status_code_when_text_is_empty(): """Test fallback to status code when text is empty.""" response = MagicMock(spec=httpx.Response) - response.status_code = EveApiResponse.SERVICE_UNAVAILABLE.value + response.status_code = HTTPStatus.SERVICE_UNAVAILABLE response.text = "" response.json.side_effect = ValueError("not json") with pytest.raises( AuthenticationError, - match=f"HTTP {EveApiResponse.SERVICE_UNAVAILABLE.value}", + match=f"HTTP {HTTPStatus.SERVICE_UNAVAILABLE}", ): EVEAuth._handle_error_response( # pylint: disable=protected-access response diff --git a/tests/test_client.py b/tests/test_client.py index b532183..bf1f568 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -5,10 +5,11 @@ import pytest from httpx import Response +from http import HTTPStatus + from eve_api import ( APIError, AuthenticationError, - EveApiResponse, EVEClient, ForbiddenError, NotAuthenticatedError, @@ -26,7 +27,7 @@ async def test_login_success(mock_api, client: EVEClient): tok = "tok-123" mock_api.post("/login").mock( return_value=Response( - EveApiResponse.SUCCESS.value, + HTTPStatus.OK, json={ "access_token": tok, "refresh_token": "ref-456", @@ -45,7 +46,7 @@ async def test_login_invalid_credentials(mock_api, client: EVEClient): """Test login for invalid credentials raises AuthenticationError""" mock_api.post("/login").mock( return_value=Response( - EveApiResponse.INVALID_CREDS.value, + HTTPStatus.UNAUTHORIZED, json={ "detail": "Invalid credentials", }, @@ -62,7 +63,7 @@ async def test_login_account_not_activated(mock_api, client: EVEClient): """Test login for non-activated account raises AuthenticationError""" mock_api.post("/login").mock( return_value=Response( - EveApiResponse.FORBIDDEN.value, + HTTPStatus.FORBIDDEN, json={ "detail": "Account not activated", }, @@ -93,7 +94,7 @@ async def test_get(mock_api, authenticated_client: EVEClient): user_email = "test@example.com" mock_api.get("/users/me").mock( return_value=Response( - EveApiResponse.SUCCESS.value, + HTTPStatus.OK, json={ "id": user_id, "email": user_email, @@ -112,7 +113,7 @@ async def test_get_with_params(mock_api, authenticated_client: EVEClient): data_name = "Public" mock_api.get("/collections/public").mock( return_value=Response( - EveApiResponse.SUCCESS.value, + HTTPStatus.OK, json={ "data": [{"id": "c-1", "name": data_name}], "meta": {"total_count": 1}, @@ -137,7 +138,7 @@ async def test_post(mock_api, authenticated_client: EVEClient): chat_name = "New Chat" mock_api.post("/conversations").mock( return_value=Response( - EveApiResponse.SUCCESS_CREATED.value, + HTTPStatus.CREATED, json={ "id": chat_id, "name": "New Chat", @@ -161,7 +162,7 @@ async def test_patch(mock_api, authenticated_client: EVEClient): new_name = "Renamed" mock_api.patch("/conversations/conv-1").mock( return_value=Response( - EveApiResponse.SUCCESS.value, + HTTPStatus.OK, json={ "id": "conv-1", "name": new_name, @@ -182,7 +183,7 @@ async def test_patch(mock_api, authenticated_client: EVEClient): async def test_delete(mock_api, authenticated_client: EVEClient): """Test deleting a conversation works when a response body is not included""" mock_api.delete("/conversations/conv-1").mock( - return_value=Response(EveApiResponse.SUCCESS_NO_CONTENT.value) + return_value=Response(HTTPStatus.NO_CONTENT) ) result = await authenticated_client.delete("/conversations/conv-1") @@ -194,7 +195,7 @@ async def test_delete_with_body(mock_api, authenticated_client: EVEClient): """Test deleting a conversation works when including a response body""" mock_api.delete("/conversations/conv-1").mock( return_value=Response( - EveApiResponse.SUCCESS.value, + HTTPStatus.OK, json={ "deleted": True, }, @@ -214,13 +215,13 @@ async def test_request_no_auth(mock_api, client: EVEClient): status = "ok" mock_api.get("/health").mock( return_value=Response( - EveApiResponse.SUCCESS.value, json={"status": status} + HTTPStatus.OK, json={"status": status} ) ) response = await client.request("GET", "/health", auth_required=False) - assert response.status_code == EveApiResponse.SUCCESS.value + assert response.status_code == HTTPStatus.OK assert response.json()["status"] == status @@ -231,7 +232,7 @@ async def test_404_raises_not_found(mock_api, authenticated_client: EVEClient): """Test that 404 response code raises NotFoundError""" mock_api.get("/conversations/missing").mock( return_value=Response( - EveApiResponse.NOT_FOUND.value, + HTTPStatus.NOT_FOUND, json={ "detail": "Not found", }, @@ -241,14 +242,14 @@ async def test_404_raises_not_found(mock_api, authenticated_client: EVEClient): with pytest.raises(NotFoundError) as exc_info: await authenticated_client.get("/conversations/missing") - assert exc_info.value.status_code == EveApiResponse.NOT_FOUND.value + assert exc_info.value.status_code == HTTPStatus.NOT_FOUND async def test_403_raises_forbidden(mock_api, authenticated_client: EVEClient): """Test that 403 response code raises ForbiddenError""" mock_api.get("/admin/users").mock( return_value=Response( - EveApiResponse.FORBIDDEN.value, + HTTPStatus.FORBIDDEN, json={ "detail": "Forbidden", }, @@ -258,7 +259,7 @@ async def test_403_raises_forbidden(mock_api, authenticated_client: EVEClient): with pytest.raises(ForbiddenError) as exc_info: await authenticated_client.get("/admin/users") - assert exc_info.value.status_code == EveApiResponse.FORBIDDEN.value + assert exc_info.value.status_code == HTTPStatus.FORBIDDEN async def test_400_raises_validation_error( @@ -267,7 +268,7 @@ async def test_400_raises_validation_error( """Test that 400 response code raises ValidationError""" mock_api.post("/conversations").mock( return_value=Response( - EveApiResponse.BAD_REQUEST.value, + HTTPStatus.BAD_REQUEST, json={ "detail": "name is required", }, @@ -277,7 +278,7 @@ async def test_400_raises_validation_error( with pytest.raises(ValidationError, match="name is required") as exc_info: await authenticated_client.post("/conversations", json={}) - assert exc_info.value.status_code == EveApiResponse.BAD_REQUEST.value + assert exc_info.value.status_code == HTTPStatus.BAD_REQUEST async def test_500_raises_server_error( @@ -286,7 +287,7 @@ async def test_500_raises_server_error( """Test that 500 response code raises ServerError""" mock_api.get("/broken").mock( return_value=Response( - EveApiResponse.INTERNAL_SERVER_ERROR.value, + HTTPStatus.INTERNAL_SERVER_ERROR, json={ "detail": "Internal server error", }, @@ -298,7 +299,7 @@ async def test_500_raises_server_error( assert ( exc_info.value.status_code - == EveApiResponse.INTERNAL_SERVER_ERROR.value + == HTTPStatus.INTERNAL_SERVER_ERROR ) @@ -350,7 +351,7 @@ def _sse_response(*events: dict, done: bool = True) -> Response: lines.append("data: [DONE]\n\n") body = "".join(lines) return Response( - EveApiResponse.SUCCESS.value, + HTTPStatus.OK, content=body.encode(), headers={"content-type": "text/event-stream"}, ) @@ -411,7 +412,7 @@ async def test_stream_error_status(mock_api, authenticated_client: EVEClient): """Test status from stream error""" mock_api.post("/conversations/c-1/stream_messages").mock( return_value=Response( - EveApiResponse.INVALID_CREDS.value, json={"detail": "Unauthorized"} + HTTPStatus.UNAUTHORIZED, json={"detail": "Unauthorized"} ) ) @@ -458,7 +459,7 @@ async def test_stream_error_invalid_json( lines.append("data: Some invalid JSON") body = "".join(lines) response = Response( - EveApiResponse.SUCCESS.value, + HTTPStatus.OK, content=body.encode(), headers={"content-type": "text/event-stream"}, ) From a6da6ee533781f83d9ed9ef187af4fe79a7f8846 Mon Sep 17 00:00:00 2001 From: William Fawcett Date: Wed, 8 Apr 2026 11:02:33 +0100 Subject: [PATCH 11/25] Add Python 3.11-3.13 CI matrix, bump minimum to 3.11, set coverage threshold to 80% --- .github/workflows/main.yml | 15 +++++++++++---- .pre-commit-config.yaml | 2 +- pyproject.toml | 4 ++-- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 62869a4..0ed73a0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,23 +3,30 @@ jobs: test: strategy: matrix: - platform: [ubuntu-latest] + python-version: ['3.11', '3.12', '3.13'] defaults: run: shell: bash - runs-on: ${{ matrix.platform }} + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: - python-version: '3.12.11' + python-version: ${{ matrix.python-version }} - name: Install Poetry uses: snok/install-poetry@v1 with: virtualenvs-create: true virtualenvs-in-project: true installer-parallel: true + - name: Cache Poetry dependencies + uses: actions/cache@v4 + with: + path: .venv + key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('poetry.lock') }} + restore-keys: | + venv-${{ runner.os }}-${{ matrix.python-version }}- - name: Install dependencies run: | poetry install --no-interaction diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1653558..5885fcb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -63,7 +63,7 @@ repos: pass_filenames: false verbose: true args: - - "--fail-under=5" + - "--fail-under=80" - "--skip-empty" - "--skip-covered" - "--show-missing" diff --git a/pyproject.toml b/pyproject.toml index 7c35e50..ecf89a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] dynamic = ["version"] name = "eve-api" -requires-python = ">=3.10" +requires-python = ">=3.11" keywords = ["earth-observation", "eve", "esa", "api-client"] [tool.poetry] @@ -34,7 +34,7 @@ version = "0.0.0" # Placeholder; gets replaced dynamically by file Repository = "https://github.com/spaceml-org/eve-api" [tool.poetry.dependencies] -python = ">=3.10,<4" +python = ">=3.11,<4" httpx = ">=0.27.0" [tool.poetry.group.dev.dependencies] From 52d95f1524b15766aaed6607852396489665f2cf Mon Sep 17 00:00:00 2001 From: William Fawcett Date: Wed, 8 Apr 2026 11:19:18 +0100 Subject: [PATCH 12/25] CI workflow fixes --- src/eve_api/__init__.py | 1 + src/eve_api/auth.py | 3 +-- src/eve_api/client.py | 3 +-- src/eve_api/exceptions.py | 3 +-- tests/conftest.py | 4 ++-- tests/test_auth.py | 3 +-- tests/test_client.py | 12 +++--------- 7 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/eve_api/__init__.py b/src/eve_api/__init__.py index f20a558..2d6fe7d 100644 --- a/src/eve_api/__init__.py +++ b/src/eve_api/__init__.py @@ -14,6 +14,7 @@ TokenExpiredError, ValidationError, ) + __all__ = [ "__version__", "EVEClient", diff --git a/src/eve_api/auth.py b/src/eve_api/auth.py index f4fe6a5..628382d 100644 --- a/src/eve_api/auth.py +++ b/src/eve_api/auth.py @@ -3,12 +3,11 @@ from __future__ import annotations from datetime import datetime, timedelta, timezone +from http import HTTPStatus from typing import Any import httpx -from http import HTTPStatus - from .exceptions import ( AuthenticationError, NotAuthenticatedError, diff --git a/src/eve_api/client.py b/src/eve_api/client.py index bb3af97..56cccbb 100644 --- a/src/eve_api/client.py +++ b/src/eve_api/client.py @@ -4,13 +4,12 @@ import json as _json from collections.abc import AsyncIterator +from http import HTTPStatus from typing import Any import httpx from .auth import EVEAuth -from http import HTTPStatus - from .exceptions import ( APIError, ForbiddenError, diff --git a/src/eve_api/exceptions.py b/src/eve_api/exceptions.py index d4ea6fe..94af460 100644 --- a/src/eve_api/exceptions.py +++ b/src/eve_api/exceptions.py @@ -2,9 +2,8 @@ from __future__ import annotations -from typing import Any - from http import HTTPStatus +from typing import Any class EVEError(Exception): diff --git a/tests/conftest.py b/tests/conftest.py index 0fbc73f..28c042e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,11 @@ """Pytest configuration and fixtures for eve-api tests.""" +from http import HTTPStatus + import pytest import respx from httpx import Response -from http import HTTPStatus - from eve_api import EVEClient diff --git a/tests/test_auth.py b/tests/test_auth.py index a98524d..7a0240c 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,13 +1,12 @@ """Tests for the EVEAuth.""" from datetime import datetime, timedelta, timezone +from http import HTTPStatus from unittest.mock import AsyncMock, MagicMock, patch import httpx import pytest -from http import HTTPStatus - from eve_api.auth import ( AuthenticationError, EVEAuth, diff --git a/tests/test_client.py b/tests/test_client.py index bf1f568..841fee8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,12 +1,11 @@ """Tests for the EVEClient.""" import json +from http import HTTPStatus import pytest from httpx import Response -from http import HTTPStatus - from eve_api import ( APIError, AuthenticationError, @@ -214,9 +213,7 @@ async def test_request_no_auth(mock_api, client: EVEClient): """Test EVEClient when auth_required=False""" status = "ok" mock_api.get("/health").mock( - return_value=Response( - HTTPStatus.OK, json={"status": status} - ) + return_value=Response(HTTPStatus.OK, json={"status": status}) ) response = await client.request("GET", "/health", auth_required=False) @@ -297,10 +294,7 @@ async def test_500_raises_server_error( with pytest.raises(ServerError) as exc_info: await authenticated_client.get("/broken") - assert ( - exc_info.value.status_code - == HTTPStatus.INTERNAL_SERVER_ERROR - ) + assert exc_info.value.status_code == HTTPStatus.INTERNAL_SERVER_ERROR async def test_422_raises_api_error(mock_api, authenticated_client: EVEClient): From ceb617a406a02ab9a28a47da80dddd59c388b841 Mon Sep 17 00:00:00 2001 From: William Fawcett Date: Wed, 8 Apr 2026 11:23:23 +0100 Subject: [PATCH 13/25] Use real EVEAuth constructor in test helpers instead of __new__ bypass --- tests/test_auth.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_auth.py b/tests/test_auth.py index 7a0240c..9a04cdd 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -21,13 +21,10 @@ def make_auth(*, access_token=None, refresh_token=None, http_client=None): """Return an Auth instance with controllable initial state.""" - auth = EVEAuth.__new__(EVEAuth) + auth = EVEAuth("https://example.com") auth.access_token = access_token auth.refresh_token = refresh_token - auth._token_expiry = None # pylint: disable=protected-access auth._http_client = http_client # pylint: disable=protected-access - # Reproduce whatever base_url the real __init__ would set; adjust if needed. - auth.base_url = "https://example.com" return auth From b690c153ff1017287f98d2d8f090b3fb767bd311 Mon Sep 17 00:00:00 2001 From: William Fawcett Date: Wed, 8 Apr 2026 11:27:53 +0100 Subject: [PATCH 14/25] Refine error names and tests to be a little more descriptive --- tests/test_client.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 841fee8..c39570f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -299,10 +299,10 @@ async def test_500_raises_server_error( async def test_422_raises_api_error(mock_api, authenticated_client: EVEClient): """Test that 422 response code raises APIError""" - random_error = 422 + unprocessable_entity = 422 mock_api.post("/conversations").mock( return_value=Response( - random_error, + unprocessable_entity, json={ "detail": [{"msg": "field required", "type": "missing"}], }, @@ -312,17 +312,17 @@ async def test_422_raises_api_error(mock_api, authenticated_client: EVEClient): with pytest.raises(APIError) as exc_info: await authenticated_client.post("/conversations", json={}) - assert exc_info.value.status_code == random_error + assert exc_info.value.status_code == unprocessable_entity -async def test_response_invalid_json_raises_api_error( +async def test_response_missing_detail_raises_api_error( mock_api, authenticated_client: EVEClient ): - """Test that invalid JSON in the response raises APIError""" - random_error = 452 + """Test that error response without 'detail' key raises APIError""" + unknown_error = 452 mock_api.post("/conversations").mock( return_value=Response( - random_error, + unknown_error, json="Some invalid JSON", ) ) @@ -330,7 +330,7 @@ async def test_response_invalid_json_raises_api_error( with pytest.raises(APIError) as exc_info: await authenticated_client.post("/conversations", json={}) - assert exc_info.value.status_code == random_error + assert exc_info.value.status_code == unknown_error # --- Streaming --- From 767839b130bbecf2a46e414eca18afebc7659cae Mon Sep 17 00:00:00 2001 From: William Fawcett Date: Wed, 8 Apr 2026 11:33:16 +0100 Subject: [PATCH 15/25] Regenerate poetry.lock after bumping minimum Python to 3.11 Removes 3.10 compatibility backports (exceptiongroup, backports-asyncio-runner, tomli) and conditional dependency markers that are no longer needed. --- poetry.lock | 116 ++-------------------------------------------------- 1 file changed, 4 insertions(+), 112 deletions(-) diff --git a/poetry.lock b/poetry.lock index 413c4f8..b674e79 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.3 and should not be changed by hand. [[package]] name = "anyio" @@ -13,7 +13,6 @@ files = [ ] [package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} @@ -32,9 +31,6 @@ files = [ {file = "astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce"}, ] -[package.dependencies] -typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} - [[package]] name = "autoflake" version = "2.3.3" @@ -49,20 +45,6 @@ files = [ [package.dependencies] pyflakes = ">=3.0.0" -tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} - -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." -optional = false -python-versions = "<3.11,>=3.8" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, - {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, -] [[package]] name = "black" @@ -102,8 +84,6 @@ mypy-extensions = ">=0.4.3" packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] @@ -418,9 +398,6 @@ files = [ {file = "coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179"}, ] -[package.dependencies] -tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} - [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] @@ -472,25 +449,6 @@ files = [ {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, ] -[[package]] -name = "exceptiongroup" -version = "1.3.1" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, - {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} - -[package.extras] -test = ["pytest (>=6)"] - [[package]] name = "filelock" version = "3.25.2" @@ -790,7 +748,6 @@ files = [ librt = {version = ">=0.8.0", markers = "platform_python_implementation != \"PyPy\""} mypy_extensions = ">=1.0.0" pathspec = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing_extensions = ">=4.6.0" [package.extras] @@ -945,14 +902,12 @@ files = [ astroid = ">=3.3.8,<=3.4.0.dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ - {version = ">=0.2", markers = "python_version < \"3.11\""}, {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, {version = ">=0.3.6", markers = "python_version == \"3.11\""}, ] isort = ">=4.2.5,<5.13 || >5.13,<7" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2" -tomli = {version = ">=1.1", markers = "python_version < \"3.11\""} tomlkit = ">=0.10.1" [package.extras] @@ -988,12 +943,10 @@ files = [ [package.dependencies] colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} iniconfig = ">=1.0.1" packaging = ">=22" pluggy = ">=1.5,<2" pygments = ">=2.7.2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] @@ -1011,7 +964,6 @@ files = [ ] [package.dependencies] -backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} pytest = ">=8.2,<10" typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} @@ -1054,7 +1006,6 @@ files = [ [package.dependencies] pytest = ">=9.0.2" python-dotenv = ">=1.2.2" -tomli = {version = ">=2.4", markers = "python_version < \"3.11\""} [package.extras] testing = ["covdefaults (>=2.3)", "coverage (>=7.13.4)", "pytest-mock (>=3.15.1)"] @@ -1247,64 +1198,6 @@ files = [ [package.dependencies] httpx = ">=0.25.0" -[[package]] -name = "tomli" -version = "2.4.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, - {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, - {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, - {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, - {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, - {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, - {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, - {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, - {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, - {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, - {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, - {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, - {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, - {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, - {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, - {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, - {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, - {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, - {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, - {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, - {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, - {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, - {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, - {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, - {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, - {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, - {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, - {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, - {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, - {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, - {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, - {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, - {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, - {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, - {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, - {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, - {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, - {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, - {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, - {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, - {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, - {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, - {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, - {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, - {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, - {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, - {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, -] - [[package]] name = "tomlkit" version = "0.14.0" @@ -1355,7 +1248,7 @@ files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] -markers = {main = "python_version <= \"3.12\""} +markers = {main = "python_version < \"3.13\""} [[package]] name = "urllib3" @@ -1392,9 +1285,8 @@ distlib = ">=0.3.7,<1" filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""} platformdirs = ">=3.9.1,<5" python-discovery = ">=1" -typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} [metadata] lock-version = "2.1" -python-versions = ">=3.10,<4" -content-hash = "84d8e6915efecb6116c920b87a7eac4578a28aee02d21f3732c71ae6b00c1798" +python-versions = ">=3.11,<4" +content-hash = "9a8a2c160a91d40bc99124d9bc1c4d92511c612d753d83dd96d417a95ca721c3" From f64385247a5002aad74678983cf7001a88674215 Mon Sep 17 00:00:00 2001 From: William Fawcett Date: Wed, 8 Apr 2026 11:49:09 +0100 Subject: [PATCH 16/25] Clean up pyproject.toml and remove unnecessary dependencies - Remove commented-out hatch/ruff config - Fix repository URL to FrontierDevelopmentLab/eve-api - Update black target-version to py312 - Simplify coverage source to just src/ (tests were included then omitted) - Remove unused type stubs (types-requests, types-toml) - Regenerate poetry.lock --- poetry.lock | 29 +---------------------------- pyproject.toml | 21 ++++----------------- 2 files changed, 5 insertions(+), 45 deletions(-) diff --git a/poetry.lock b/poetry.lock index b674e79..a2f4801 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1210,33 +1210,6 @@ files = [ {file = "tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064"}, ] -[[package]] -name = "types-requests" -version = "2.33.0.20260402" -description = "Typing stubs for requests" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "types_requests-2.33.0.20260402-py3-none-any.whl", hash = "sha256:c98372d7124dd5d10af815ee25c013897592ff92af27b27e22c98984102c3254"}, - {file = "types_requests-2.33.0.20260402.tar.gz", hash = "sha256:1bdd3ada9b869741c5c4b887d2c8b4e38284a1449751823b5ebbccba3eefd9da"}, -] - -[package.dependencies] -urllib3 = ">=2" - -[[package]] -name = "types-toml" -version = "0.10.8.20240310" -description = "Typing stubs for toml" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "types-toml-0.10.8.20240310.tar.gz", hash = "sha256:3d41501302972436a6b8b239c850b26689657e25281b48ff0ec06345b8830331"}, - {file = "types_toml-0.10.8.20240310-py3-none-any.whl", hash = "sha256:627b47775d25fa29977d9c70dc0cbab3f314f32c8d8d0c012f2ef5de7aaec05d"}, -] - [[package]] name = "typing-extensions" version = "4.15.0" @@ -1289,4 +1262,4 @@ python-discovery = ">=1" [metadata] lock-version = "2.1" python-versions = ">=3.11,<4" -content-hash = "9a8a2c160a91d40bc99124d9bc1c4d92511c612d753d83dd96d417a95ca721c3" +content-hash = "5ce75a6ae222fbe4f770c11c4dbe2ddda9025047da33b9100f5b992b7ae40103" diff --git a/pyproject.toml b/pyproject.toml index ecf89a3..5214643 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,9 +21,9 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering", "Framework :: AsyncIO", ] @@ -31,7 +31,7 @@ packages = [{include = "eve_api", from = "src"}] version = "0.0.0" # Placeholder; gets replaced dynamically by file [project.urls] -Repository = "https://github.com/spaceml-org/eve-api" +Repository = "https://github.com/FrontierDevelopmentLab/eve-api" [tool.poetry.dependencies] python = ">=3.11,<4" @@ -51,15 +51,13 @@ pytest-cov = ">=4.0.0" pytest-testdox = "^3.1.0" pytest-mock = "^3.14.0" detect-secrets = "^1.5.0" -types-requests = "^2.32.4.20250611" pytest-env = "^1.1.5" -types-toml = "^0.10.8.20240310" respx = ">=0.21.0" pylint-per-file-ignores = "^3.2.0" [tool.black] line-length = 79 -target-version = ['py311'] +target-version = ['py312'] include = '\.pyi?$' extend-exclude = ''' /( @@ -103,7 +101,7 @@ testpaths = ["tests"] addopts = "-v --tb=short --basetemp=/tmp/pytest" [tool.coverage.run] -source = ["src", "tests"] +source = ["src"] omit = ["tests/**/conftest.py", "tests/*"] data_file = "/tmp/eve_api/.coverage" @@ -141,17 +139,6 @@ vcs = "none" source = "src/eve_api/_version.py" pattern = "^__version__\\s*=\\s*[\"']?(\\d+\\.\\d+\\.\\d+)[\"']?" -# [tool.hatch.build.targets.wheel] -# packages = ["src/eve_api"] - -# [tool.ruff] -# line-length = 100 -# target-version = "py310" - -# [tool.ruff.lint] -# select = ["E", "F", "I", "W"] -# ignore = ["E501"] - [build-system] requires = ["poetry-core", "poetry-dynamic-versioning>=1.0.0,<2.0.0"] build-backend = "poetry_dynamic_versioning.backend" From c4ccf9c8af3e24bacf133c24b62108f9494a68ed Mon Sep 17 00:00:00 2001 From: William Fawcett Date: Wed, 8 Apr 2026 11:54:24 +0100 Subject: [PATCH 17/25] Add Python 3.14 to CI matrix and classifiers --- .github/workflows/main.yml | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0ed73a0..491d575 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,7 +3,7 @@ jobs: test: strategy: matrix: - python-version: ['3.11', '3.12', '3.13'] + python-version: ['3.11', '3.12', '3.13', '3.14'] defaults: run: shell: bash diff --git a/pyproject.toml b/pyproject.toml index 5214643..3e22927 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering", "Framework :: AsyncIO", ] From 869e0410c6b4bc69eea31002078782ef7817fd31 Mon Sep 17 00:00:00 2001 From: William Fawcett Date: Thu, 30 Apr 2026 09:40:00 +0100 Subject: [PATCH 18/25] docs: add CI, Python, and license badges to README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 193f67e..2799742 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # eve-api +[![CI](https://github.com/FrontierDevelopmentLab/eve-api/actions/workflows/main.yml/badge.svg)](https://github.com/FrontierDevelopmentLab/eve-api/actions/workflows/main.yml) +[![Python](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://github.com/FrontierDevelopmentLab/eve-api) +[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE) + Minimal authenticated HTTP client for the EVE (Earth Virtual Expert) API. Provides login, automatic JWT token refresh, and generic HTTP methods that return plain dicts. No domain-specific wrappers or Pydantic models. From ead0fa27d52a085258b6923414ee3fa38c101a5f Mon Sep 17 00:00:00 2001 From: William Fawcett Date: Thu, 30 Apr 2026 09:40:05 +0100 Subject: [PATCH 19/25] fix: preserve server detail message in NotFoundError NotFoundError previously required (resource, resource_id) and the 404 branch in _handle_error passed hardcoded ("resource", "unknown"), discarding the parsed `detail` from the response body. Switch the constructor to accept a message + details (matching the other APIError subclasses) and forward the parsed message through. --- src/eve_api/client.py | 2 +- src/eve_api/exceptions.py | 15 ++++++++++----- tests/test_client.py | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/eve_api/client.py b/src/eve_api/client.py index 56cccbb..99bff05 100644 --- a/src/eve_api/client.py +++ b/src/eve_api/client.py @@ -356,7 +356,7 @@ def _handle_error(response: httpx.Response) -> None: message = response.text or f"HTTP {status}" if status == HTTPStatus.NOT_FOUND: - raise NotFoundError("resource", "unknown") + raise NotFoundError(message) if status == HTTPStatus.FORBIDDEN: raise ForbiddenError(message) if status == HTTPStatus.BAD_REQUEST: diff --git a/src/eve_api/exceptions.py b/src/eve_api/exceptions.py index 94af460..78cc0b3 100644 --- a/src/eve_api/exceptions.py +++ b/src/eve_api/exceptions.py @@ -58,17 +58,22 @@ def __init__( class NotFoundError(APIError): """Raised when a requested resource is not found (404).""" - def __init__(self, resource: str, resource_id: str) -> None: + def __init__( + self, + message: str = "Resource not found", + details: dict[str, Any] | None = None, + ) -> None: """Initialise the not found error. Args: - resource: Type of resource (e.g., 'conversation', 'document'). - resource_id: ID of the resource that was not found. + message: Human-readable error message (typically the server's + ``detail`` field). + details: Additional error details from the response body. """ super().__init__( - f"{resource.title()} not found: {resource_id}", + message, status_code=HTTPStatus.NOT_FOUND, - details={"resource": resource, "id": resource_id}, + details=details, ) diff --git a/tests/test_client.py b/tests/test_client.py index c39570f..32dad99 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -236,7 +236,7 @@ async def test_404_raises_not_found(mock_api, authenticated_client: EVEClient): ) ) - with pytest.raises(NotFoundError) as exc_info: + with pytest.raises(NotFoundError, match="Not found") as exc_info: await authenticated_client.get("/conversations/missing") assert exc_info.value.status_code == HTTPStatus.NOT_FOUND From f5f2960645c48717e4dbadf57e9c7d6b6788b5c6 Mon Sep 17 00:00:00 2001 From: William Fawcett Date: Thu, 30 Apr 2026 17:05:31 +0100 Subject: [PATCH 20/25] Adding TypedDict for test SSE event classes --- tests/test_client.py | 55 ++++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 32dad99..19c2204 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -2,6 +2,7 @@ import json from http import HTTPStatus +from typing import Literal, TypedDict import pytest from httpx import Response @@ -18,6 +19,30 @@ ValidationError, ) + +class StatusEvent(TypedDict): + type: Literal["status"] + content: str + + +class TokenEvent(TypedDict): + type: Literal["token"] + content: str + + +class FinalEvent(TypedDict): + type: Literal["final"] + content: str + message_id: str + + +class ErrorEvent(TypedDict): + type: Literal["error"] + content: str + + +SSEEvent = StatusEvent | TokenEvent | FinalEvent | ErrorEvent + # --- Authentication --- @@ -336,7 +361,7 @@ async def test_response_missing_detail_raises_api_error( # --- Streaming --- -def _sse_response(*events: dict, done: bool = True) -> Response: +def _sse_response(*events: SSEEvent, done: bool = True) -> Response: """Build a Response whose body is SSE-formatted lines.""" lines = [] for event in events: @@ -355,11 +380,11 @@ async def test_stream(mock_api, authenticated_client: EVEClient): """Test streaming""" status_text = "status" final_text = "final" - events = [ - {"type": status_text, "content": "Searching..."}, - {"type": "token", "content": "Hello"}, - {"type": "token", "content": " world"}, - {"type": final_text, "content": "Hello world", "message_id": "m-1"}, + events: list[SSEEvent] = [ + StatusEvent(type="status", content="Searching..."), + TokenEvent(type="token", content="Hello"), + TokenEvent(type="token", content=" world"), + FinalEvent(type="final", content="Hello world", message_id="m-1"), ] mock_api.post("/conversations/c-1/stream_messages").mock( return_value=_sse_response(*events) @@ -383,9 +408,9 @@ async def test_stream_stops_on_error_event( ): """Test that streaming stops on an error event""" error_text = "error" - events = [ - {"type": "token", "content": "partial"}, - {"type": error_text, "content": "Something went wrong"}, + events: list[SSEEvent] = [ + TokenEvent(type="token", content="partial"), + ErrorEvent(type="error", content="Something went wrong"), ] mock_api.post("/conversations/c-1/stream_messages").mock( return_value=_sse_response(*events, done=False) @@ -420,9 +445,9 @@ async def test_stream_error_status(mock_api, authenticated_client: EVEClient): async def test_stream_done_no_error(mock_api, authenticated_client: EVEClient): """Test streaming when it returns data: [DONE] with no errors""" - events = [ - {"type": "token", "content": "Hello"}, - {"type": "token", "content": " world"}, + events: list[SSEEvent] = [ + TokenEvent(type="token", content="Hello"), + TokenEvent(type="token", content=" world"), ] mock_api.post("/conversations/c-1/stream_messages").mock( return_value=_sse_response(*events) @@ -443,9 +468,9 @@ async def test_stream_error_invalid_json( mock_api, authenticated_client: EVEClient ): """Test status from stream error""" - events = [ - {"type": "token", "content": "Hello"}, - {"type": "token", "content": " world"}, + events: list[SSEEvent] = [ + TokenEvent(type="token", content="Hello"), + TokenEvent(type="token", content=" world"), ] lines = [] for event in events: From 3115bf25a4797758f96bfedddff8f2c34275d4c3 Mon Sep 17 00:00:00 2001 From: William Fawcett Date: Thu, 30 Apr 2026 17:07:58 +0100 Subject: [PATCH 21/25] Remove magic 500, match directly with response.text --- tests/test_auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_auth.py b/tests/test_auth.py index 9a04cdd..80d589a 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -201,7 +201,7 @@ async def test_refresh_clears_tokens_on_expired_refresh_token(): async def test_refresh_unexpected_status_calls_handle_error(): """Test that refresh on unexpected status calls _handle_error.""" auth = make_auth(refresh_token="ref") - response = make_response(500) + response = make_response(HTTPStatus.INTERNAL_SERVER_ERROR) mock_client = AsyncMock(spec=httpx.AsyncClient) mock_client.post.return_value = response @@ -449,7 +449,7 @@ def test_falls_back_to_response_text_when_json_fails(): response.status_code = HTTPStatus.SERVICE_UNAVAILABLE response.text = "Service Unavailable" response.json.side_effect = ValueError("not json") - with pytest.raises(AuthenticationError, match="Service Unavailable"): + with pytest.raises(AuthenticationError, match=response.text): EVEAuth._handle_error_response( # pylint: disable=protected-access response ) From 06629c32f5c4528703f12c9894261d0a73288934 Mon Sep 17 00:00:00 2001 From: William Fawcett Date: Thu, 30 Apr 2026 17:13:06 +0100 Subject: [PATCH 22/25] Add missing docstrings to classes --- src/eve_api/client.py | 5 ++--- tests/test_client.py | 8 ++++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/eve_api/client.py b/src/eve_api/client.py index 99bff05..9b18c3d 100644 --- a/src/eve_api/client.py +++ b/src/eve_api/client.py @@ -39,6 +39,7 @@ class EVEClient: """ _DEFAULT_TIMEOUT = 30.0 + _SSE_DONE_SENTINEL = "[DONE]" def __init__( self, @@ -260,9 +261,7 @@ async def stream( if not line or not line.startswith("data: "): continue - if ( # pylint: disable=magic-value-comparison - data_str := line[6:] - ) == "[DONE]": + if (data_str := line[6:]) == self._SSE_DONE_SENTINEL: return try: diff --git a/tests/test_client.py b/tests/test_client.py index 19c2204..ac272a8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -21,22 +21,30 @@ class StatusEvent(TypedDict): + """SSE event emitted while the server is still working.""" + type: Literal["status"] content: str class TokenEvent(TypedDict): + """SSE event carrying an incremental token of the streamed response.""" + type: Literal["token"] content: str class FinalEvent(TypedDict): + """SSE event marking the final, complete response.""" + type: Literal["final"] content: str message_id: str class ErrorEvent(TypedDict): + """SSE event indicating the stream terminated with an error.""" + type: Literal["error"] content: str From e34eb10bd3737348c018293fd5c4496f0a27301a Mon Sep 17 00:00:00 2001 From: William Fawcett Date: Thu, 30 Apr 2026 17:15:01 +0100 Subject: [PATCH 23/25] bump coverage to 100 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5885fcb..1f56240 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -63,7 +63,7 @@ repos: pass_filenames: false verbose: true args: - - "--fail-under=80" + - "--fail-under=100" - "--skip-empty" - "--skip-covered" - "--show-missing" From 9d0ba7f3face829b01c86df64dddb0f01a81ecc0 Mon Sep 17 00:00:00 2001 From: William Fawcett Date: Thu, 30 Apr 2026 17:21:18 +0100 Subject: [PATCH 24/25] Add remaining email address --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3e22927..73da50c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,8 +11,8 @@ readme = "README.md" authors = [ "r-spiewak <63987228+r-spiewak@users.noreply.github.com>", "dead-water ", - "rramosp", - "will-fawcett-trillium" + "rramosp <13835425+rramosp@users.noreply.github.com>", + "will-fawcett-trillium <192252394+will-fawcett-trillium@users.noreply.github.com>" ] classifiers = [ "Development Status :: 3 - Alpha", From e4bd25382cbafbd25555e9c67103c2c20cc63603 Mon Sep 17 00:00:00 2001 From: William Fawcett Date: Fri, 1 May 2026 15:06:44 +0100 Subject: [PATCH 25/25] Remove magic 422 --- tests/test_client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index ac272a8..a0f3dce 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -332,10 +332,9 @@ async def test_500_raises_server_error( async def test_422_raises_api_error(mock_api, authenticated_client: EVEClient): """Test that 422 response code raises APIError""" - unprocessable_entity = 422 mock_api.post("/conversations").mock( return_value=Response( - unprocessable_entity, + HTTPStatus.UNPROCESSABLE_ENTITY, json={ "detail": [{"msg": "field required", "type": "missing"}], }, @@ -345,7 +344,7 @@ async def test_422_raises_api_error(mock_api, authenticated_client: EVEClient): with pytest.raises(APIError) as exc_info: await authenticated_client.post("/conversations", json={}) - assert exc_info.value.status_code == unprocessable_entity + assert exc_info.value.status_code == HTTPStatus.UNPROCESSABLE_ENTITY async def test_response_missing_detail_raises_api_error(