diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3689eeba..568c6bbd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -33,8 +33,8 @@ jobs: working-directory: fastapi_startkit run: uv run ruff format --check . - pyright: - name: Pyright (non-blocking) + basedpyright: + name: Basedpyright runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -51,9 +51,6 @@ jobs: working-directory: fastapi_startkit run: uv sync --group dev - # Type checking is advisory while the existing baseline is worked down; - # continue-on-error keeps a failing run from blocking the pipeline. - - name: Run pyright - continue-on-error: true + - name: Run basedpyright working-directory: fastapi_startkit - run: uv run pyright + run: uv run basedpyright diff --git a/fastapi_startkit/CHANGELOG.md b/fastapi_startkit/CHANGELOG.md deleted file mode 100644 index 81740b90..00000000 --- a/fastapi_startkit/CHANGELOG.md +++ /dev/null @@ -1,41 +0,0 @@ -# Changelog - -## Unreleased - -### `serve` command defaults now come from `FastAPIConfig` - -`ServeCommand` no longer restates defaults that `FastAPIConfig` already declares. -Every server setting resolves as **CLI flag > `fastapi` config > `FastAPIConfig` default**. - -- **New `FastAPIConfig.app`** field (`"bootstrap.application:app"`), so the served - entrypoint is configurable like every other setting. Add it to your - `config/fastapi.py` if you want to override it: - - ```python - app: str = "bootstrap.application:app" - ``` - -- **Behaviour change:** an application that registers no `fastapi` config now gets - `reload_excludes` (`["*.log", "tests/*", "node_modules/*"]`) passed to uvicorn when - reload is on. Previously these were only forwarded when a config was registered, so - such an app watched excluded paths. Set `reload_excludes = []` in your `fastapi` - config to restore the old behaviour. - -- A `fastapi` config key that is present but set to `None` now falls back to the - `FastAPIConfig` default instead of being forwarded as `None`. - -## 0.48.0 - -### Breaking changes - -The top-level `fastapi_startkit.providers` package has been removed. Its -contents moved into `foundation` and `support`, and there is **no** -backward-compatibility shim — consumers must update their imports: - -| Old import | New import | -|---|---| -| `from fastapi_startkit.providers import Provider` | `from fastapi_startkit.support import Provider` | -| `from fastapi_startkit.providers.app_provider import ...` | `from fastapi_startkit.foundation.app_provider import ...` | -| `from fastapi_startkit.helpers.dataclass import ...` | `from fastapi_startkit.support.dataclass import ...` | - -`fastapi_startkit.helpers.app` has been removed. diff --git a/fastapi_startkit/pyproject.toml b/fastapi_startkit/pyproject.toml index 6dc1843c..02a10736 100644 --- a/fastapi_startkit/pyproject.toml +++ b/fastapi_startkit/pyproject.toml @@ -107,6 +107,7 @@ dev = [ "faker>=40.13.0", "langchain>=1.0.0", "langchain-core>=1.0.0", + "basedpyright>=1.31.4", "pyright>=1.1.411", "pytest-benchmark>=5.2.3", ] @@ -122,7 +123,7 @@ fixable = ["F401"] [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] -[tool.pyright] +[tool.basedpyright] include = ["src/fastapi_startkit"] exclude = [ "**/tests", diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/__init__.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/__init__.py index 7c7cf005..c7e892d0 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/__init__.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/__init__.py @@ -1,3 +1,6 @@ from .model import Model from .caster import Caster from .registry import Registry +from .fields import Field, ModelField + +__all__ = ["Model", "Caster", "Registry", "Field", "ModelField"] diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/caster.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/caster.py index 7f2172d9..fe970781 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/caster.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/caster.py @@ -4,8 +4,9 @@ from decimal import Decimal from enum import Enum from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, get_type_hints, Optional +from typing import TYPE_CHECKING, Any, get_args, get_type_hints, Optional from pydantic.fields import FieldInfo +from pydantic import BaseModel as PydanticModel from fastapi_startkit.carbon import Carbon if TYPE_CHECKING: @@ -214,7 +215,7 @@ def build_casts(cls, model): # Ignore the builder annotations = {k: v for k, v in annotations.items() if k not in cls.IGNORE_CASTS} - from .fields import ModelField, FieldDescriptor + from .fields import FieldDescriptor, ModelField # 1. Collect all potential fields (annotations + descriptors) all_field_names = set(annotations.keys()) @@ -226,11 +227,30 @@ def build_casts(cls, model): casts = {} for field_name in all_field_names: - typ = annotations.get(field_name) or "str" descriptor = descriptors.get(field_name, None) + typ = annotations.get(field_name) - # AttributeField: use the type annotation as the model class - if isinstance(descriptor, ModelField): + # ``Field[int]()`` carries its runtime type in ``__orig_class__``. + # This lets models use typed descriptors without repeating an + # annotation solely for the casting layer. + if typ is None and isinstance(descriptor, FieldDescriptor): + generic_args = get_args(getattr(descriptor, "__orig_class__", None)) + if generic_args: + typ = generic_args[0] + + # An unsubscripted field can still derive its cast from a concrete + # default, as in ``Field(default=False)``. + if typ is None and isinstance(descriptor, FieldDescriptor): + from pydantic_core import PydanticUndefined + + if descriptor.field_info.default is not PydanticUndefined: + typ = type(descriptor.field_info.default) + + typ = typ or "str" + + # Nested Pydantic models are stored as JSON and hydrated back into + # their declared type, e.g. ``address = Field[Address]()``. + if isinstance(descriptor, ModelField) or (isinstance(typ, type) and issubclass(typ, PydanticModel)): casts[field_name] = ModelCast(model_class=typ) continue diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/fields.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/fields.py index 4140dc6b..4c1905ac 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/fields.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/fields.py @@ -1,6 +1,8 @@ -from pydantic.fields import FieldInfo +from typing import Any, Callable, Generic, Protocol, Self, TypeVar, overload +import warnings + from pydantic import Field as BaseField -from typing import Any +from pydantic.fields import FieldInfo from fastapi_startkit.masoniteorm.models.observer import ( CreatedAtObserver, @@ -8,49 +10,99 @@ ) -class FieldDescriptor: +T = TypeVar("T") + + +class _AttributeModel(Protocol): + def get_attribute(self, key: str) -> Any: ... + + def set_attribute(self, key: str, value: Any) -> None: ... + + +class FieldDescriptor(Generic[T]): """ A descriptor that wraps Pydantic's FieldInfo. It allows us to store metadata that the Caster can later discover. """ - def __init__(self, field_info: FieldInfo): + def __init__(self, field_info: FieldInfo) -> None: self.field_info = field_info - self.name = None + self.name: str | None = None - def __set_name__(self, owner, name): + def __set_name__(self, owner: type[_AttributeModel], name: str) -> None: self.name = name - def __get__(self, instance, owner): + @overload + def __get__(self, instance: None, owner: type[_AttributeModel]) -> FieldInfo: ... + + @overload + def __get__(self, instance: _AttributeModel, owner: type[_AttributeModel]) -> T: ... + + def __get__(self, instance: _AttributeModel | None, owner: type[_AttributeModel]) -> T | FieldInfo: if instance is None: # When accessed on the class (e.g., User.name), return the FieldInfo return self.field_info # When accessed on the instance (e.g., user.name), retrieve from ORM storage + assert self.name is not None return instance.get_attribute(self.name) - def __set__(self, instance, value): + def __set__(self, instance: _AttributeModel, value: T) -> None: # When setting (e.g., user.name = 'Joe'), update ORM storage - instance.set_value(self.name, value) + assert self.name is not None + instance.set_attribute(self.name, value) -def Field(*args, **kwargs) -> Any: +class Field(FieldDescriptor[T]): """ - Factory function that returns a FieldDescriptor wrapping a Pydantic Field. + Typed ORM field descriptor backed by Pydantic field metadata. + + Required fields can state their type explicitly with ``Field[int]()``. + Fields with a default infer their type with ``Field(default=False)``. """ - return FieldDescriptor(BaseField(*args, **kwargs)) + @overload + def __init__(self, *, default: T, default_factory: None = None, **kwargs: Any) -> None: ... -class ModelField: - def __set_name__(self, owner, name): + @overload + def __init__(self, *, default_factory: Callable[[], T], **kwargs: Any) -> None: ... + + @overload + def __init__(self, *args: Any, **kwargs: Any) -> None: ... + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(BaseField(*args, **kwargs)) + + +class ModelField(Generic[T]): + """Deprecated; scheduled for removal in 2.x. Use ``Field[Address]()`` instead.""" + + def __init__(self, default: T | None = None) -> None: + warnings.warn( + "ModelField is deprecated and will be removed in 2.x; use Field[YourModel]() instead.", + DeprecationWarning, + stacklevel=2, + ) + self.default = default + self.name: str | None = None + + def __set_name__(self, owner: type[_AttributeModel], name: str) -> None: self.name = name - def __get__(self, instance, owner): + @overload + def __get__(self, instance: None, owner: type[_AttributeModel]) -> Self: ... + + @overload + def __get__(self, instance: _AttributeModel, owner: type[_AttributeModel]) -> T: ... + + def __get__(self, instance: _AttributeModel | None, owner: type[_AttributeModel]) -> T | Self: if instance is None: return self + assert self.name is not None return instance.get_attribute(self.name) - def __set__(self, instance, value): + def __set__(self, instance: _AttributeModel, value: T) -> None: + assert self.name is not None instance.set_attribute(self.name, value) @@ -105,4 +157,4 @@ def __get__(self, instance, owner): return instance.get_attribute(self.name) def __set__(self, instance, value): - instance.set_value(self.name, value) + instance.set_attribute(self.name, value) diff --git a/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/model.py b/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/model.py index a7705e95..9b07a23d 100644 --- a/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/model.py +++ b/fastapi_startkit/src/fastapi_startkit/masoniteorm/models/model.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Self, overload +from typing import TYPE_CHECKING, Any, Self, dataclass_transform, overload import inflection import pendulum @@ -9,7 +9,13 @@ from fastapi_startkit.masoniteorm.collection import Collection from fastapi_startkit.masoniteorm.connections.manager import DatabaseManager from fastapi_startkit.masoniteorm.models.attribute import Attribute -from fastapi_startkit.masoniteorm.models.fields import CreatedAtField, UpdatedAtField +from fastapi_startkit.masoniteorm.models.fields import ( + CreatedAtField, + Field, + FieldDescriptor, + ModelField, + UpdatedAtField, +) from fastapi_startkit.masoniteorm.models.registry import Registry from fastapi_startkit.masoniteorm.models.relationship import Relationship from fastapi_startkit.masoniteorm.observers import ObservesEvents @@ -18,6 +24,7 @@ from fastapi_startkit.masoniteorm.models.builder import QueryBuilder, WhereGroup +@dataclass_transform(field_specifiers=(Field, ModelField)) class Model(Attribute, Relationship, ObservesEvents): db_manager: "DatabaseManager" = None __table__ = None @@ -34,9 +41,16 @@ def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) Registry.register(cls) + declared_fields = dict.fromkeys( + [ + *cls.__annotations__, + *(name for name, value in vars(cls).items() if isinstance(value, FieldDescriptor)), + ] + ) + fillable = [] - for name, _typ in cls.__annotations__.items(): - attr = getattr(cls, name, None) + for name in declared_fields: + attr = vars(cls).get(name) from fastapi_startkit.masoniteorm.relationships.BaseRelationship import ( BaseRelationship, ) diff --git a/fastapi_startkit/tests/masoniteorm/fixtures/model.py b/fastapi_startkit/tests/masoniteorm/fixtures/model.py index 5abbc181..c746b402 100644 --- a/fastapi_startkit/tests/masoniteorm/fixtures/model.py +++ b/fastapi_startkit/tests/masoniteorm/fixtures/model.py @@ -2,7 +2,7 @@ from fastapi_startkit.carbon.carbon import Carbon from tests.masoniteorm.fixtures.casts import Address -from fastapi_startkit.masoniteorm import ModelField, Field +from fastapi_startkit.masoniteorm import Field from fastapi_startkit.masoniteorm import ( HasOne, BelongsTo, @@ -16,16 +16,16 @@ class User(Model): - id: int - name: str - email: str + id = Field[int]() + name = Field[str]() + email = Field[str]() email_verified_at: datetime date_of_birth: date session_duration: timedelta punch_in_time: time = Field(default=time(12, 0, 0)) - is_admin: bool + is_admin = Field(default=False) preferences: dict - address: Address = ModelField() + address = Field[Address]() profile: "Profile" = HasOne("Profile", "user_id", "id") articles: "Articles" = HasMany("Articles", "id", "user_id") diff --git a/fastapi_startkit/tests/masoniteorm/models/test_model.py b/fastapi_startkit/tests/masoniteorm/models/test_model.py index 88ebdbc6..6ddea25d 100644 --- a/fastapi_startkit/tests/masoniteorm/models/test_model.py +++ b/fastapi_startkit/tests/masoniteorm/models/test_model.py @@ -1,4 +1,5 @@ from fastapi_startkit.masoniteorm.models.model import Model +from fastapi_startkit.masoniteorm.models.fields import Field from tests.masoniteorm.fixtures.model import User from tests.masoniteorm.sqlite.test_case import TestCase @@ -114,6 +115,15 @@ class Post(Model): assert "title" in Post.__fillable__ assert "body" in Post.__fillable__ + async def test_typed_fields_are_in_fillable(self): + class Post(Model): + __table__ = "posts" + title = Field[str]() + published = Field(default=False) + + assert "title" in Post.__fillable__ + assert "published" in Post.__fillable__ + async def test_framework_fields_excluded_from_fillable(self): class Post(Model): __table__ = "posts" diff --git a/fastapi_startkit/tests/masoniteorm/models/test_model_attributes.py b/fastapi_startkit/tests/masoniteorm/models/test_model_attributes.py index b3957105..58da9f3f 100644 --- a/fastapi_startkit/tests/masoniteorm/models/test_model_attributes.py +++ b/fastapi_startkit/tests/masoniteorm/models/test_model_attributes.py @@ -24,6 +24,111 @@ } +def test_deprecated_model_field_remains_compatible(): + from fastapi_startkit.masoniteorm import ModelField + from tests.masoniteorm.fixtures.casts import Address + + with pytest.warns(DeprecationWarning, match="use Field"): + + class LegacyUser(Model): + address: Address = ModelField() + + user = LegacyUser(address={"city": "Sydney"}) + assert isinstance(user.address, Address) + assert user.address.city == "Sydney" + assert isinstance(LegacyUser.address, ModelField) + user.address = Address(city="Melbourne") + assert user.address.city == "Melbourne" + restored = LegacyUser(user.get_attributes()) + assert restored.address.city == "Melbourne" + + +@pytest.mark.parametrize("mixed_fields", [False, True], ids=["annotation-only", "mixed-fields"]) +def test_annotation_only_columns_remain_supported(mixed_fields): + from fastapi_startkit.masoniteorm import Field + + if mixed_fields: + + class CompatibleUser(Model): + id: int + name: str + email: str + score = Field[int]() + is_admin = Field(default=False) + + else: + + class CompatibleUser(Model): + id: int + name: str + email: str + + # Hydration from raw storage still uses plain annotations for casting. + user = CompatibleUser({"id": "42", "name": "Alex", "email": "alex@example.com", "score": "7"}) + assert user.id == 42 + assert isinstance(user.id, int) + assert user.name == "Alex" + assert user.email == "alex@example.com" + + user.name = "Jane" + user.fill({"email": "jane@example.com"}) + assert user.name == "Jane" + assert user.email == "jane@example.com" + assert {"id", "name", "email"} <= set(CompatibleUser.__fillable__) + + if mixed_fields: + assert user.score == 7 + assert isinstance(user.score, int) + assert user.is_admin is False + user.fill({"score": 9, "is_admin": True}) + assert user.score == 9 + assert user.is_admin is True + + +def test_field_descriptor_assignment_casts_and_tracks_dirty_values(): + from fastapi_startkit.masoniteorm import Field + + class DescriptorUser(Model): + id = Field[int]() + + user = DescriptorUser({"id": 1}) + user.sync_original() + # Bypass Model.__setattr__ to exercise Python's descriptor protocol. + object.__setattr__(user, "id", "42") + assert user.id == 42 + assert user.get_dirty() == {"id": 42} + assert user._original["id"] == 1 + + +def test_legacy_model_field_descriptor_assignment_serializes_pydantic_values(): + import json + + from fastapi_startkit.masoniteorm import ModelField + from tests.masoniteorm.fixtures.casts import Address + + with pytest.warns(DeprecationWarning, match="removed in 2.x"): + + class DescriptorLegacyUser(Model): + address: Address = ModelField() + + user = DescriptorLegacyUser() + object.__setattr__(user, "address", Address(city="Sydney")) + assert isinstance(user.address, Address) + assert user.address.city == "Sydney" + assert json.loads(user.get_dirty()["address"])["city"] == "Sydney" + + +def test_updated_at_descriptor_assignment_uses_attribute_storage(): + class TimestampUser(Model): + pass + + user = TimestampUser() + timestamp = pendulum.datetime(2026, 1, 2, 3, 4, 5, tz="UTC") + object.__setattr__(user, "updated_at", timestamp) + assert user.updated_at == timestamp + assert user.get_dirty()["updated_at"] == "2026-01-02 03:04:05" + + @pytest.fixture async def db(): manager = DatabaseManager(ConnectionFactory(), SQLITE_CONFIG) diff --git a/fastapi_startkit/tests/utils/test_structures.py b/fastapi_startkit/tests/utils/test_structures.py index 12b1e9f2..6a460ae6 100644 --- a/fastapi_startkit/tests/utils/test_structures.py +++ b/fastapi_startkit/tests/utils/test_structures.py @@ -9,7 +9,7 @@ from dotty_dict import Dotty from fastapi_startkit.exceptions.exceptions import LoaderNotFound -from fastapi_startkit.utils.structures import data, data_get, data_set, load +from fastapi_startkit.support.structures import data, data_get, data_set, load MODULE_SOURCE = "VALUE = 42\n\n\ndef greet():\n return 'hi'\n" diff --git a/fastapi_startkit/uv.lock b/fastapi_startkit/uv.lock index fe15d144..ca0be247 100644 --- a/fastapi_startkit/uv.lock +++ b/fastapi_startkit/uv.lock @@ -99,6 +99,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, ] +[[package]] +name = "basedpyright" +version = "1.40.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodejs-wheel-binaries" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/ee/8d0b6806338b13526303cf72754351221a32cdb80ab56dcf2c91d6b1ea57/basedpyright-1.40.1.tar.gz", hash = "sha256:da1c9913b6d169340a0dbb6df76ea97f476ccb697da8f92659ac46032f6d2ce8", size = 25131254, upload-time = "2026-09-10T23:17:24.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/84/c1e1e845d0453253a98d1ba188d68597f0ba7e552eeabea514d9395d418a/basedpyright-1.40.1-py3-none-any.whl", hash = "sha256:222dc0382caf9816eb23a27cb7059fa96356ddc500b3b7b306072848fd650244", size = 13689276, upload-time = "2026-09-10T23:17:20.322Z" }, +] + [[package]] name = "certifi" version = "2026.4.22" @@ -527,7 +539,7 @@ wheels = [ [[package]] name = "fastapi-startkit" -version = "0.51.0" +version = "0.56.0" source = { editable = "." } dependencies = [ { name = "cleo" }, @@ -575,6 +587,7 @@ dev = [ { name = "aiomysql" }, { name = "aiosqlite" }, { name = "asyncpg" }, + { name = "basedpyright" }, { name = "dumpdie" }, { name = "faker" }, { name = "fastapi", extra = ["standard"] }, @@ -621,6 +634,7 @@ dev = [ { name = "aiomysql", specifier = ">=0.2.0" }, { name = "aiosqlite", specifier = ">=0.22.1" }, { name = "asyncpg", specifier = ">=0.29.0" }, + { name = "basedpyright", specifier = ">=1.31.4" }, { name = "dumpdie", specifier = ">=1.5.0" }, { name = "faker", specifier = ">=40.13.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.124.4" }, @@ -1224,6 +1238,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] +[[package]] +name = "nodejs-wheel-binaries" +version = "24.19.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/76/7e97195e14346598565a0de4ca8bdbd5e634b3fb5b1ba590b7b1b89f8a63/nodejs_wheel_binaries-24.19.0.tar.gz", hash = "sha256:db217eef8cab8551667863379b08db4d9067403f6cbbe87481eb40edceb8aa9b", size = 8058, upload-time = "2026-08-19T21:47:19.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/52/0774b52c7be8151ad9d5aff44edc100c3f13d6d8eb3765f63ffa40e69fe8/nodejs_wheel_binaries-24.19.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:e12cbfd69089504e42fb14194ce734a9dcf3eb38c820ca63dd511d36fb964e9c", size = 56047203, upload-time = "2026-08-19T21:46:43.448Z" }, + { url = "https://files.pythonhosted.org/packages/67/3a/4fdbbfecf2c23d52c0e3f68de7f7c1b3c97a26d328c69c5f6c49c48e340e/nodejs_wheel_binaries-24.19.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:1c890adf4b7e6556ccc1ca66c866bb81884b6c9a581dee4e530dc7f78fb9d514", size = 56219459, upload-time = "2026-08-19T21:46:48.45Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a8/0147149415195c59b8a72a594916bfb80d6be4d586f9fbfda313889e0efc/nodejs_wheel_binaries-24.19.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:4e029dadfae1295876063c96b236f673487e8c27379fe146c1e2250283520227", size = 60588256, upload-time = "2026-08-19T21:46:53.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/89/6631d0982353da1bb7bc00bb1988f702822c62b42634f57999ee53b5c337/nodejs_wheel_binaries-24.19.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:4196a947bcc883f2003ab101762d729f3e99b5e86b75bd09151563403e2eceb8", size = 61123607, upload-time = "2026-08-19T21:46:58.117Z" }, + { url = "https://files.pythonhosted.org/packages/32/a2/fa30f0841e4602995782e124359f9b910c7b481d98decf61ef0b2fc3ebfb/nodejs_wheel_binaries-24.19.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:352e048ab4dd35e7de5f338d1cc4fcbf77a0e93da30bf7336a8217ee246b31d7", size = 62632842, upload-time = "2026-08-19T21:47:03.42Z" }, + { url = "https://files.pythonhosted.org/packages/18/01/22d97ca72213f66cc386ee638db30c2e62757fdf761c6029074bced83d1c/nodejs_wheel_binaries-24.19.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:28d078b2ced9e2069516e652dc4b1380e7a1a7f2d3934eccd1611586d283ba4c", size = 63250653, upload-time = "2026-08-19T21:47:07.938Z" }, + { url = "https://files.pythonhosted.org/packages/88/d1/e3be8fa327a795bcaf7a19cd84299e338a7bce32ff0665fdce9cfa22573c/nodejs_wheel_binaries-24.19.0-py2.py3-none-win_amd64.whl", hash = "sha256:67e3abeb9c3830cae8c8487ae8a2af7cc27dfa75af06145cee5ca7d1857c81bd", size = 42448503, upload-time = "2026-08-19T21:47:12.093Z" }, + { url = "https://files.pythonhosted.org/packages/1d/37/34cf28ba1691a060174948a9927fe61091982d6048b2e403071a9acce443/nodejs_wheel_binaries-24.19.0-py2.py3-none-win_arm64.whl", hash = "sha256:d9074c665ea68b04e183d82482c86dc907d3a9bd15eb6cf85542cb785266bb36", size = 40090155, upload-time = "2026-08-19T21:47:16.032Z" }, +] + [[package]] name = "orjson" version = "3.11.9" @@ -1510,15 +1540,15 @@ wheels = [ [[package]] name = "pyright" -version = "1.1.411" +version = "1.1.414" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/1b/244c7b710031ada80f27e579ec20d28a2285dfc318fed0339866b1047f12/pyright-1.1.414.tar.gz", hash = "sha256:523c0a97c60da6333234955c277730c9cf4f5bd6d5399e7b7d2b0fc5d3599524", size = 4154638, upload-time = "2026-09-10T12:26:53.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ba/18b6e682ead424ad24bcc134339ae5d1b931cd9ae260540592a058a91279/pyright-1.1.414-py3-none-any.whl", hash = "sha256:2a6b4b3298c9eec174c5ed83bd338de6eee82df2992f3e1930e6199d381be36f", size = 6225049, upload-time = "2026-09-10T12:26:51.427Z" }, ] [[package]]