diff --git a/docs/conf.py b/docs/conf.py index 6e17794..4fe8992 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -133,7 +133,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = [] +html_static_path: list[str] = [] # If not "", a "Last updated on:" timestamp is inserted at every page bottom, # using the given strftime format. @@ -182,14 +182,14 @@ # -- Options for LaTeX output -------------------------------------------------- -latex_elements = { - # The paper size ("letterpaper" or "a4paper"). - # "papersize": "letterpaper", - # The font size ("10pt", "11pt" or "12pt"). - # "pointsize": "10pt", - # Additional stuff for the LaTeX preamble. - # "preamble": "", -} +# latex_elements = { +# The paper size ("letterpaper" or "a4paper"). +# "papersize": "letterpaper", +# The font size ("10pt", "11pt" or "12pt"). +# "pointsize": "10pt", +# Additional stuff for the LaTeX preamble. +# "preamble": "", +# } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). @@ -228,9 +228,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ("index", "flask-pymongo", "Flask-PyMongo Documentation", ["Dan Crosta"], 1) -] +man_pages = [("index", "flask-pymongo", "Flask-PyMongo Documentation", ["Dan Crosta"], 1)] # If true, show URL addresses after external links. # man_show_urls = False diff --git a/examples/wiki/wiki.py b/examples/wiki/wiki.py index b4b64ec..ed29fda 100644 --- a/examples/wiki/wiki.py +++ b/examples/wiki/wiki.py @@ -1,12 +1,16 @@ from __future__ import annotations import re +from typing import TYPE_CHECKING, Any -import markdown2 +import markdown2 # type:ignore[import-untyped] from flask import Flask, redirect, render_template, request, url_for from flask_pymongo import PyMongo +if TYPE_CHECKING: + from werkzeug.wrappers.response import Response + app = Flask(__name__) mongo = PyMongo(app, "mongodb://localhost/wiki") @@ -15,17 +19,17 @@ @app.route("/", methods=["GET"]) -def redirect_to_homepage(): +def redirect_to_homepage() -> Response: return redirect(url_for("show_page", pagepath="HomePage")) @app.template_filter() -def totitle(value): +def totitle(value: str) -> str: return " ".join(WIKIPART.findall(value)) @app.template_filter() -def wikify(value): +def wikify(value: str) -> Any: parts = WIKIWORD.split(value) for i, part in enumerate(parts): if WIKIWORD.match(part): @@ -36,20 +40,23 @@ def wikify(value): @app.route("/") -def show_page(pagepath): - page = mongo.db.pages.find_one_or_404({"_id": pagepath}) +def show_page(pagepath: str) -> str: + assert mongo.db is not None + page: dict[str, Any] = mongo.db.pages.find_one_or_404({"_id": pagepath}) return render_template("page.html", page=page, pagepath=pagepath) @app.route("/edit/", methods=["GET"]) -def edit_page(pagepath): - page = mongo.db.pages.find_one_or_404({"_id": pagepath}) +def edit_page(pagepath: str) -> str: + assert mongo.db is not None + page: dict[str, Any] = mongo.db.pages.find_one_or_404({"_id": pagepath}) return render_template("edit.html", page=page, pagepath=pagepath) @app.route("/edit/", methods=["POST"]) -def save_page(pagepath): +def save_page(pagepath: str) -> Response: if "cancel" not in request.form: + assert mongo.db is not None mongo.db.pages.update( {"_id": pagepath}, {"$set": {"body": request.form["body"]}}, @@ -60,7 +67,7 @@ def save_page(pagepath): @app.errorhandler(404) -def new_page(error): +def new_page(error: Any) -> str: pagepath = request.path.lstrip("/") if pagepath.startswith("uploads"): filename = pagepath[len("uploads") :].lstrip("/") @@ -69,12 +76,12 @@ def new_page(error): @app.route("/uploads/") -def get_upload(filename): +def get_upload(filename: str) -> Response: return mongo.send_file(filename) @app.route("/uploads/", methods=["POST"]) -def save_upload(filename): +def save_upload(filename: str) -> str | Response: if request.files.get("file"): mongo.save_file(filename, request.files["file"]) return redirect(url_for("get_upload", filename=filename)) diff --git a/flask_pymongo/__init__.py b/flask_pymongo/__init__.py index e1de50c..72aef75 100644 --- a/flask_pymongo/__init__.py +++ b/flask_pymongo/__init__.py @@ -24,26 +24,22 @@ # POSSIBILITY OF SUCH DAMAGE. from __future__ import annotations -__all__ = ("PyMongo", "ASCENDING", "DESCENDING") +__all__ = ("PyMongo", "ASCENDING", "DESCENDING", "BSONObjectIdConverter", "BSONProvider") import hashlib from mimetypes import guess_type +from typing import Any import pymongo -from flask import abort, current_app, request +from flask import Flask, Response, abort, current_app, request from gridfs import GridFS, NoFile from pymongo import uri_parser +from pymongo.driver_info import DriverInfo from werkzeug.wsgi import wrap_file -# DriverInfo was added in PyMongo 3.7 -try: - from pymongo.driver_info import DriverInfo -except ImportError: - DriverInfo = None - from flask_pymongo._version import __version__ from flask_pymongo.helpers import BSONObjectIdConverter, BSONProvider -from flask_pymongo.wrappers import MongoClient +from flask_pymongo.wrappers import Database, MongoClient DESCENDING = pymongo.DESCENDING """Descending sort order.""" @@ -65,15 +61,16 @@ class PyMongo: """ - def __init__(self, app=None, uri=None, *args, **kwargs): - self.cx = None - self.db = None - self._json_provider = BSONProvider(app) + def __init__( + self, app: Flask | None = None, uri: str | None = None, *args: Any, **kwargs: Any + ) -> None: + self.cx: MongoClient | None = None + self.db: Database | None = None if app is not None: self.init_app(app, uri, *args, **kwargs) - def init_app(self, app, uri=None, *args, **kwargs): + def init_app(self, app: Flask, uri: str | None = None, *args: Any, **kwargs: Any) -> None: """Initialize this :class:`PyMongo` for use. Configure a :class:`~pymongo.mongo_client.MongoClient` @@ -122,10 +119,12 @@ def init_app(self, app, uri=None, *args, **kwargs): self.db = self.cx[database_name] app.url_map.converters["ObjectId"] = BSONObjectIdConverter - app.json = self._json_provider + app.json = BSONProvider(app) # view helpers - def send_file(self, filename, base="fs", version=-1, cache_for=31536000): + def send_file( + self, filename: str, base: str = "fs", version: int = -1, cache_for: int = 31536000 + ) -> Response: """Respond with a file from GridFS. Returns an instance of the :attr:`~flask.Flask.response_class` @@ -153,6 +152,7 @@ def get_upload(filename): if not isinstance(cache_for, int): raise TypeError("'cache_for' must be an integer") + assert self.db is not None, "Please initialize the app before calling send_file!" storage = GridFS(self.db, base) try: @@ -183,7 +183,14 @@ def get_upload(filename): response.make_conditional(request) return response - def save_file(self, filename, fileobj, base="fs", content_type=None, **kwargs): + def save_file( + self, + filename: str, + fileobj: Any, + base: str = "fs", + content_type: str | None = None, + **kwargs: Any, + ) -> Any: """Save a file-like object to GridFS using the given filename. Return the "_id" of the created file. @@ -211,8 +218,7 @@ def save_upload(filename): if content_type is None: content_type, _ = guess_type(filename) + assert self.db is not None, "Please initialize the app before calling save_file!" storage = GridFS(self.db, base) - id = storage.put( - fileobj, filename=filename, content_type=content_type, **kwargs - ) + id = storage.put(fileobj, filename=filename, content_type=content_type, **kwargs) return id diff --git a/flask_pymongo/helpers.py b/flask_pymongo/helpers.py index 2660562..ed09d11 100644 --- a/flask_pymongo/helpers.py +++ b/flask_pymongo/helpers.py @@ -26,6 +26,8 @@ __all__ = ("BSONObjectIdConverter", "BSONProvider") +from typing import Any + from bson import json_util from bson.errors import InvalidId from bson.json_util import RELAXED_JSON_OPTIONS @@ -35,7 +37,7 @@ from werkzeug.routing import BaseConverter -def _iteritems(obj): +def _iteritems(obj: Any) -> Any: if hasattr(obj, "iteritems"): return obj.iteritems() if hasattr(obj, "items"): @@ -65,13 +67,13 @@ def show_task(task_id): """ - def to_python(self, value): + def to_python(self, value: Any) -> ObjectId: try: return ObjectId(value) except InvalidId: raise abort(404) from None - def to_url(self, value): + def to_url(self, value: Any) -> str: return str(value) @@ -98,15 +100,15 @@ def json_route(cart_id): :const:`~bson.json_util.RELAXED_JSON_OPTIONS`. """ - def __init__(self, app): + def __init__(self, app: Any) -> None: self._default_kwargs = {"json_options": RELAXED_JSON_OPTIONS} super().__init__(app) - def dumps(self, obj): + def dumps(self, obj: Any, **kwargs: Any) -> str: """Serialize MongoDB object types using :mod:`bson.json_util`.""" return json_util.dumps(obj) - def loads(self, str_obj): + def loads(self, s: str | bytes, **kwargs: Any) -> Any: """Deserialize MongoDB object types using :mod:`bson.json_util`.""" - return json_util.loads(str_obj) + return json_util.loads(s) diff --git a/flask_pymongo/tests/__init__.py b/flask_pymongo/py.typed similarity index 100% rename from flask_pymongo/tests/__init__.py rename to flask_pymongo/py.typed diff --git a/flask_pymongo/wrappers.py b/flask_pymongo/wrappers.py index dd30d49..fc543df 100644 --- a/flask_pymongo/wrappers.py +++ b/flask_pymongo/wrappers.py @@ -24,11 +24,13 @@ # POSSIBILITY OF SUCH DAMAGE. from __future__ import annotations +from typing import Any + from flask import abort from pymongo import collection, database, mongo_client -class MongoClient(mongo_client.MongoClient): +class MongoClient(mongo_client.MongoClient[dict[str, Any]]): """Wrapper for :class:`~pymongo.mongo_client.MongoClient`. Returns instances of Flask-PyMongo @@ -37,20 +39,20 @@ class MongoClient(mongo_client.MongoClient): """ - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any: attr = super().__getattr__(name) if isinstance(attr, database.Database): return Database(self, name) return attr - def __getitem__(self, item): - attr = super().__getitem__(item) + def __getitem__(self, name: str) -> Any: + attr = super().__getitem__(name) if isinstance(attr, database.Database): - return Database(self, item) + return Database(self, name) return attr -class Database(database.Database): +class Database(database.Database[dict[str, Any]]): """Wrapper for :class:`~pymongo.database.Database`. Returns instances of Flask-PyMongo @@ -59,37 +61,37 @@ class Database(database.Database): """ - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any: attr = super().__getattr__(name) if isinstance(attr, collection.Collection): return Collection(self, name) return attr - def __getitem__(self, item): - item_ = super().__getitem__(item) + def __getitem__(self, name: str) -> Any: + item_ = super().__getitem__(name) if isinstance(item_, collection.Collection): - return Collection(self, item) + return Collection(self, name) return item_ -class Collection(collection.Collection): +class Collection(collection.Collection[dict[str, Any]]): """Sub-class of PyMongo :class:`~pymongo.collection.Collection` with helpers.""" - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any: attr = super().__getattr__(name) if isinstance(attr, collection.Collection): db = self._Collection__database return Collection(db, attr.name) return attr - def __getitem__(self, item): - item_ = super().__getitem__(item) - if isinstance(item_, collection.Collection): + def __getitem__(self, name: str) -> Any: + item = super().__getitem__(name) + if isinstance(item, collection.Collection): db = self._Collection__database - return Collection(db, item_.name) - return item_ + return Collection(db, item.name) + return item - def find_one_or_404(self, *args, **kwargs): + def find_one_or_404(self, *args: Any, **kwargs: Any) -> Any: """Find a single document or raise a 404. This is like :meth:`~pymongo.collection.Collection.find_one`, but diff --git a/justfile b/justfile index cd2a9f5..e5fc302 100644 --- a/justfile +++ b/justfile @@ -16,3 +16,6 @@ lint: docs: uv run sphinx-build -T -b html docs docs/_build + +typing: + uv run mypy --install-types --non-interactive . diff --git a/pyproject.toml b/pyproject.toml index 26264e0..bcc9b12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,15 @@ dependencies = [ Download = "https://github.com/mongodb-labs/flask-pymongo/tags" Homepage = "http://flask-pymongo.readthedocs.org/" +[dependency-groups] +dev = [ + "markdown2>=2.5.2", + "mypy>=1.14.1", + "pre-commit>=4.0.1", + "pytest>=8.3.4", + "sphinx>=7.4.7", +] + [tool.hatch.version] path = "flask_pymongo/_version.py" @@ -45,6 +54,18 @@ include = [ "/flask_pymongo", ] +[tool.mypy] +python_version = "3.9" +strict = true +enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] + +[[tool.mypy.overrides]] +module = ["tests.*"] +disable_error_code = ["no-untyped-def", "no-untyped-call"] + +[tool.ruff] +line-length = 100 + [tool.ruff.lint] select = [ "E", # pycodestyle @@ -58,11 +79,3 @@ unfixable = [ "T20", # Removes print statements "F401", # Unused imports ] - -[dependency-groups] -dev = [ - "markdown2>=2.5.2", - "pre-commit>=4.0.1", - "pytest>=8.3.4", - "sphinx>=7.4.7", -] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flask_pymongo/tests/test_config.py b/tests/test_config.py similarity index 87% rename from flask_pymongo/tests/test_config.py rename to tests/test_config.py index 9667da0..ddf5fe3 100644 --- a/flask_pymongo/tests/test_config.py +++ b/tests/test_config.py @@ -2,12 +2,14 @@ import time from contextlib import contextmanager +from typing import Any import pymongo import pytest import flask_pymongo -from flask_pymongo.tests.util import FlaskRequestTest + +from .util import FlaskRequestTest class CouldNotConnect(Exception): @@ -26,16 +28,18 @@ class FlaskPyMongoConfigTest(FlaskRequestTest): def setUp(self): super().setUp() - conn = pymongo.MongoClient(port=self.port) + conn: pymongo.MongoClient[Any] = pymongo.MongoClient(port=self.port) conn.test.command("ping") # wait for server + conn.close() def tearDown(self): super().tearDown() - conn = pymongo.MongoClient(port=self.port) + conn: pymongo.MongoClient[Any] = pymongo.MongoClient(port=self.port) conn.drop_database(self.dbname) conn.drop_database(self.dbname + "2") + conn.close() def test_config_with_uri_in_flask_conf_var(self): uri = f"mongodb://localhost:{self.port}/{self.dbname}" @@ -44,6 +48,8 @@ def test_config_with_uri_in_flask_conf_var(self): mongo = flask_pymongo.PyMongo(self.app, connect=True) _wait_until_connected(mongo) + assert mongo.db is not None + assert mongo.cx is not None assert mongo.db.name == self.dbname assert ("localhost", self.port) == mongo.cx.address or ( "127.0.0.1", @@ -56,6 +62,8 @@ def test_config_with_uri_passed_directly(self): mongo = flask_pymongo.PyMongo(self.app, uri, connect=True) _wait_until_connected(mongo) + assert mongo.db is not None + assert mongo.cx is not None assert mongo.db.name == self.dbname assert ("localhost", self.port) == mongo.cx.address or ( "127.0.0.1", @@ -78,11 +86,12 @@ def test_multiple_pymongos(self): # this test passes if it raises no exceptions def test_custom_document_class(self): - class CustomDict(dict): + class CustomDict(dict[str, Any]): pass uri = f"mongodb://localhost:{self.port}/{self.dbname}" mongo = flask_pymongo.PyMongo(self.app, uri, document_class=CustomDict) + assert mongo.db is not None assert mongo.db.things.find_one() is None, "precondition failed" mongo.db.things.insert_one({"_id": "thing", "val": "foo"}) diff --git a/flask_pymongo/tests/test_gridfs.py b/tests/test_gridfs.py similarity index 90% rename from flask_pymongo/tests/test_gridfs.py rename to tests/test_gridfs.py index c778167..93a6672 100644 --- a/flask_pymongo/tests/test_gridfs.py +++ b/tests/test_gridfs.py @@ -8,17 +8,17 @@ from gridfs import GridFS from werkzeug.exceptions import NotFound -from flask_pymongo.tests.util import FlaskPyMongoTest +from .util import FlaskPyMongoTest class GridFSCleanupMixin: def tearDown(self): - gridfs = GridFS(self.mongo.db) + gridfs = GridFS(self.mongo.db) # type:ignore[attr-defined] files = list(gridfs.find()) for gridfile in files: gridfs.delete(gridfile._id) - super().tearDown() + super().tearDown() # type:ignore[misc] class TestSaveFile(GridFSCleanupMixin, FlaskPyMongoTest): @@ -26,7 +26,7 @@ def test_it_saves_files(self): fileobj = BytesIO(b"these are the bytes") self.mongo.save_file("my-file", fileobj) - + assert self.mongo.db is not None gridfs = GridFS(self.mongo.db) assert gridfs.exists({"filename": "my-file"}) @@ -35,8 +35,10 @@ def test_it_saves_files_with_props(self): self.mongo.save_file("my-file", fileobj, foo="bar") + assert self.mongo.db is not None gridfs = GridFS(self.mongo.db) gridfile = gridfs.find_one({"filename": "my-file"}) + assert gridfile is not None assert gridfile.foo == "bar" def test_it_returns_id(self): diff --git a/flask_pymongo/tests/test_json.py b/tests/test_json.py similarity index 92% rename from flask_pymongo/tests/test_json.py rename to tests/test_json.py index 424d7b5..753d624 100644 --- a/flask_pymongo/tests/test_json.py +++ b/tests/test_json.py @@ -5,7 +5,7 @@ from bson import ObjectId from flask import jsonify -from flask_pymongo.tests.util import FlaskPyMongoTest +from .util import FlaskPyMongoTest class JSONTest(FlaskPyMongoTest): @@ -20,8 +20,8 @@ def test_it_handles_pymongo_types(self): self.assertEqual(dumped, {"id": {"$oid": "5cf29abb5167a14c9e6e12c4"}}) def test_it_jsonifies_a_cursor(self): + assert self.mongo.db is not None self.mongo.db.rows.insert_many([{"foo": "bar"}, {"foo": "baz"}]) - curs = self.mongo.db.rows.find(projection={"_id": False}).sort("foo") resp = jsonify(curs) diff --git a/flask_pymongo/tests/test_url_converter.py b/tests/test_url_converter.py similarity index 64% rename from flask_pymongo/tests/test_url_converter.py rename to tests/test_url_converter.py index a7e4950..46b4bc2 100644 --- a/flask_pymongo/tests/test_url_converter.py +++ b/tests/test_url_converter.py @@ -2,20 +2,19 @@ from bson import ObjectId from werkzeug.exceptions import NotFound +from werkzeug.routing.map import Map from flask_pymongo import BSONObjectIdConverter -from flask_pymongo.tests.util import FlaskPyMongoTest + +from .util import FlaskPyMongoTest class UrlConverterTest(FlaskPyMongoTest): def test_bson_object_id_converter(self): - converter = BSONObjectIdConverter("/") + converter = BSONObjectIdConverter(Map()) self.assertRaises(NotFound, converter.to_python, ("132")) assert converter.to_python("4e4ac5cfffc84958fa1f45fb") == ObjectId( "4e4ac5cfffc84958fa1f45fb" ) - assert ( - converter.to_url(ObjectId("4e4ac5cfffc84958fa1f45fb")) - == "4e4ac5cfffc84958fa1f45fb" - ) + assert converter.to_url(ObjectId("4e4ac5cfffc84958fa1f45fb")) == "4e4ac5cfffc84958fa1f45fb" diff --git a/flask_pymongo/tests/test_wrappers.py b/tests/test_wrappers.py similarity index 69% rename from flask_pymongo/tests/test_wrappers.py rename to tests/test_wrappers.py index ca87b13..39af363 100644 --- a/flask_pymongo/tests/test_wrappers.py +++ b/tests/test_wrappers.py @@ -1,12 +1,15 @@ from __future__ import annotations +from typing import Any + from werkzeug.exceptions import HTTPException -from flask_pymongo.tests.util import FlaskPyMongoTest +from .util import FlaskPyMongoTest class CollectionTest(FlaskPyMongoTest): def test_find_one_or_404(self): + assert self.mongo.db is not None self.mongo.db.things.delete_many({}) try: @@ -17,5 +20,5 @@ def test_find_one_or_404(self): self.mongo.db.things.insert_one({"_id": "thing", "val": "foo"}) # now it should not raise - thing = self.mongo.db.things.find_one_or_404({"_id": "thing"}) - assert thing["val"] == "foo", "got wrong thing" + thing: dict[str, Any] = self.mongo.db.things.find_one_or_404({"_id": "thing"}) + assert thing["val"] == "foo" diff --git a/flask_pymongo/tests/util.py b/tests/util.py similarity index 94% rename from flask_pymongo/tests/util.py rename to tests/util.py index 7372e16..9f2ab83 100644 --- a/flask_pymongo/tests/util.py +++ b/tests/util.py @@ -31,6 +31,7 @@ def setUp(self): self.mongo = flask_pymongo.PyMongo(self.app, uri) def tearDown(self): + assert self.mongo.cx is not None self.mongo.cx.drop_database(self.dbname) super().tearDown() diff --git a/uv.lock b/uv.lock index 327029b..b1c40c1 100644 --- a/uv.lock +++ b/uv.lock @@ -234,6 +234,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "markdown2" }, + { name = "mypy" }, { name = "pre-commit" }, { name = "pytest" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -249,6 +250,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "markdown2", specifier = ">=2.5.2" }, + { name = "mypy", specifier = ">=1.14.1" }, { name = "pre-commit", specifier = ">=4.0.1" }, { name = "pytest", specifier = ">=8.3.4" }, { name = "sphinx", specifier = ">=7.4.7" }, @@ -400,6 +402,59 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506 }, ] +[[package]] +name = "mypy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002 }, + { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400 }, + { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172 }, + { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732 }, + { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197 }, + { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836 }, + { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432 }, + { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515 }, + { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791 }, + { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203 }, + { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900 }, + { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869 }, + { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668 }, + { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060 }, + { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167 }, + { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341 }, + { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991 }, + { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016 }, + { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097 }, + { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728 }, + { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965 }, + { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660 }, + { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198 }, + { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276 }, + { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493 }, + { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702 }, + { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104 }, + { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167 }, + { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834 }, + { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231 }, + { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -769,6 +824,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, ] +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + [[package]] name = "urllib3" version = "2.3.0"