diff --git a/pyairtable/__init__.py b/pyairtable/__init__.py index 8847f334..da712f33 100644 --- a/pyairtable/__init__.py +++ b/pyairtable/__init__.py @@ -1,9 +1,9 @@ __version__ = "3.0.0" -from .api import Api, Base, Table -from .api.enterprise import Enterprise -from .api.retrying import retry_strategy -from .api.workspace import Workspace +from pyairtable.api import Api, Base, Table +from pyairtable.api.enterprise import Enterprise +from pyairtable.api.retrying import retry_strategy +from pyairtable.api.workspace import Workspace __all__ = [ "Api", diff --git a/pyairtable/api/__init__.py b/pyairtable/api/__init__.py index 724aacb8..151feda3 100644 --- a/pyairtable/api/__init__.py +++ b/pyairtable/api/__init__.py @@ -1,6 +1,6 @@ -from .api import Api -from .base import Base -from .table import Table +from pyairtable.api.api import Api +from pyairtable.api.base import Base +from pyairtable.api.table import Table __all__ = [ "Api", diff --git a/pyairtable/api/api.py b/pyairtable/api/api.py index c8924890..ac1d8633 100644 --- a/pyairtable/api/api.py +++ b/pyairtable/api/api.py @@ -6,8 +6,10 @@ from typing_extensions import TypeAlias from pyairtable.api import retrying +from pyairtable.api.base import Base from pyairtable.api.enterprise import Enterprise from pyairtable.api.params import options_to_json_and_params, options_to_params +from pyairtable.api.table import Table from pyairtable.api.types import UserAndScopesDict, assert_typed_dict from pyairtable.api.workspace import Workspace from pyairtable.models.schema import Bases @@ -44,7 +46,7 @@ class Api: MAX_URL_LENGTH = 16000 # Cached metadata to reduce API calls - _bases: Optional[Dict[str, "pyairtable.api.base.Base"]] = None + _bases: Optional[Dict[str, "Base"]] = None endpoint_url: Url session: Session @@ -126,7 +128,7 @@ def base( *, validate: bool = False, force: bool = False, - ) -> "pyairtable.api.base.Base": + ) -> "Base": """ Return a new :class:`Base` instance that uses this instance of :class:`Api`. @@ -141,7 +143,7 @@ def base( if validate: info = self._base_info(force=force).base(base_id) return self._base_from_info(info) - return pyairtable.api.base.Base(self, base_id) + return Base(self, base_id) @cache_unless_forced def _base_info(self) -> Bases: @@ -158,15 +160,15 @@ def _base_info(self) -> Bases: } return Bases.from_api(data, self) - def _base_from_info(self, base_info: Bases.Info) -> "pyairtable.api.base.Base": - return pyairtable.api.base.Base( + def _base_from_info(self, base_info: Bases.Info) -> "Base": + return Base( self, base_info.id, name=base_info.name, permission_level=base_info.permission_level, ) - def bases(self, *, force: bool = False) -> List["pyairtable.api.base.Base"]: + def bases(self, *, force: bool = False) -> List["Base"]: """ Retrieve the base's schema and return a list of :class:`Base` instances. @@ -189,7 +191,7 @@ def create_base( workspace_id: str, name: str, tables: Sequence[Dict[str, Any]], - ) -> "pyairtable.api.base.Base": + ) -> "Base": """ Create a base in the given workspace. @@ -210,7 +212,7 @@ def table( *, validate: bool = False, force: bool = False, - ) -> "pyairtable.api.table.Table": + ) -> "Table": """ Build a new :class:`Table` instance that uses this instance of :class:`Api`. @@ -407,7 +409,3 @@ def enterprise(self, enterprise_account_id: str) -> Enterprise: Build an object representing an enterprise account. """ return Enterprise(self, enterprise_account_id) - - -import pyairtable.api.base # noqa -import pyairtable.api.table # noqa diff --git a/pyairtable/api/base.py b/pyairtable/api/base.py index 317bf365..2efd8ef2 100644 --- a/pyairtable/api/base.py +++ b/pyairtable/api/base.py @@ -1,8 +1,7 @@ import warnings from functools import cached_property -from typing import Any, Dict, List, Optional, Sequence, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Union -import pyairtable.api.api import pyairtable.api.table from pyairtable.models.schema import BaseCollaborators, BaseSchema, BaseShares from pyairtable.models.webhook import ( @@ -13,6 +12,9 @@ ) from pyairtable.utils import Url, UrlBuilder, cache_unless_forced, enterprise_only +if TYPE_CHECKING: + from pyairtable.api.api import Api + class Base: """ @@ -25,7 +27,7 @@ class Base: """ #: The connection to the Airtable API. - api: "pyairtable.api.api.Api" + api: "Api" #: The base ID, in the format ``appXXXXXXXXXXXXXX`` id: str @@ -67,7 +69,7 @@ def interface(self, interface_id: str) -> Url: def __init__( self, - api: Union["pyairtable.api.api.Api", str], + api: Union["Api", str], base_id: str, *, name: Optional[str] = None, @@ -99,7 +101,10 @@ def __init__( category=DeprecationWarning, stacklevel=2, ) - api = pyairtable.api.api.Api(api) + + from pyairtable import Api + + api = Api(api) self.api = api self.id = base_id diff --git a/pyairtable/api/enterprise.py b/pyairtable/api/enterprise.py index 557097e0..8183a55f 100644 --- a/pyairtable/api/enterprise.py +++ b/pyairtable/api/enterprise.py @@ -1,6 +1,16 @@ from datetime import date, datetime from functools import cached_property, partialmethod -from typing import Any, Dict, Iterable, Iterator, List, Literal, Optional, Union +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Iterable, + Iterator, + List, + Literal, + Optional, + Union, +) import pydantic from typing_extensions import Self @@ -17,6 +27,9 @@ enterprise_only, ) +if TYPE_CHECKING: + from pyairtable.api.api import Api + @enterprise_only class Enterprise: @@ -85,7 +98,7 @@ def remove_user(self, user_id: str) -> Url: urls = cached_property(_urls) - def __init__(self, api: "pyairtable.api.api.Api", workspace_id: str): + def __init__(self, api: "Api", workspace_id: str): self.api = api self.id = workspace_id self._info: Optional[EnterpriseInfo] = None @@ -612,8 +625,3 @@ class MoveWorkspacesResponse(AirtableModel): rebuild_models(vars()) - - -# These are at the bottom of the module to avoid circular imports -import pyairtable.api.api # noqa -import pyairtable.api.base # noqa diff --git a/pyairtable/api/table.py b/pyairtable/api/table.py index 235b5989..96b2f0a4 100644 --- a/pyairtable/api/table.py +++ b/pyairtable/api/table.py @@ -5,10 +5,19 @@ import warnings from functools import cached_property from pathlib import Path -from typing import Any, Dict, Iterable, Iterator, List, Optional, Union, overload +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Iterable, + Iterator, + List, + Optional, + Union, + overload, +) import pyairtable.models -from pyairtable.api.retrying import Retry from pyairtable.api.types import ( FieldName, RecordDeletedDict, @@ -25,6 +34,11 @@ from pyairtable.models.schema import FieldSchema, TableSchema, parse_field_schema from pyairtable.utils import Url, UrlBuilder, is_table_id +if TYPE_CHECKING: + from pyairtable.api.api import Api, TimeoutTuple + from pyairtable.api.base import Base + from pyairtable.api.retrying import Retry + class Table: """ @@ -37,7 +51,7 @@ class Table: """ #: The base that this table belongs to. - base: "pyairtable.api.base.Base" + base: "Base" #: Can be either the table name or the table ID (``tblXXXXXXXXXXXXXX``). name: str @@ -82,8 +96,8 @@ def __init__( base_id: str, table_name: str, *, - timeout: Optional["pyairtable.api.api.TimeoutTuple"] = None, - retry_strategy: Optional[Retry] = None, + timeout: Optional["TimeoutTuple"] = None, + retry_strategy: Optional["Retry"] = None, endpoint_url: str = "https://api.airtable.com", ): ... @@ -91,7 +105,7 @@ def __init__( def __init__( self, api_key: None, - base_id: "pyairtable.api.base.Base", + base_id: "Base", table_name: str, ): ... @@ -99,14 +113,14 @@ def __init__( def __init__( self, api_key: None, - base_id: "pyairtable.api.base.Base", + base_id: "Base", table_name: TableSchema, ): ... def __init__( self, api_key: Union[None, str], - base_id: Union["pyairtable.api.base.Base", str], + base_id: Union["Base", str], table_name: Union[str, TableSchema], **kwargs: Any, ): @@ -210,7 +224,7 @@ def id_or_name(self, quoted: bool = True) -> str: return value @property - def api(self) -> "pyairtable.api.api.Api": + def api(self) -> "Api": """ The API connection used by the table's :class:`~pyairtable.Base`. """ @@ -801,8 +815,3 @@ def upload_attachment( } response = self.api.post(url, json=payload) return assert_typed_dict(UploadAttachmentResultDict, response) - - -# These are at the bottom of the module to avoid circular imports -import pyairtable.api.api # noqa -import pyairtable.api.base # noqa diff --git a/pyairtable/api/workspace.py b/pyairtable/api/workspace.py index 5293d4eb..7a63763c 100644 --- a/pyairtable/api/workspace.py +++ b/pyairtable/api/workspace.py @@ -1,9 +1,13 @@ from functools import cached_property -from typing import Any, Dict, List, Optional, Sequence, Union +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Union from pyairtable.models.schema import WorkspaceCollaborators from pyairtable.utils import Url, UrlBuilder, cache_unless_forced, enterprise_only +if TYPE_CHECKING: + from pyairtable.api.api import Api + from pyairtable.api.base import Base + class Workspace: """ @@ -33,7 +37,7 @@ class _urls(UrlBuilder): urls = cached_property(_urls) - def __init__(self, api: "pyairtable.api.api.Api", workspace_id: str): + def __init__(self, api: "Api", workspace_id: str): self.api = api self.id = workspace_id @@ -41,7 +45,7 @@ def create_base( self, name: str, tables: Sequence[Dict[str, Any]], - ) -> "pyairtable.api.base.Base": + ) -> "Base": """ Create a base in the given workspace. @@ -73,7 +77,7 @@ def collaborators(self) -> WorkspaceCollaborators: return WorkspaceCollaborators.from_api(payload, self.api, context=self) @enterprise_only - def bases(self) -> List["pyairtable.api.base.Base"]: + def bases(self) -> List["Base"]: """ Retrieve all bases within the workspace. """ @@ -103,7 +107,7 @@ def delete(self) -> None: @enterprise_only def move_base( self, - base: Union[str, "pyairtable.api.base.Base"], + base: Union[str, "Base"], target: Union[str, "Workspace"], index: Optional[int] = None, ) -> None: @@ -123,8 +127,3 @@ def move_base( if index is not None: payload["targetIndex"] = index self.api.post(self.urls.move_base, json=payload) - - -# These are at the bottom of the module to avoid circular imports -import pyairtable.api.api # noqa -import pyairtable.api.base # noqa diff --git a/pyairtable/models/__init__.py b/pyairtable/models/__init__.py index 765a613f..ebf6a764 100644 --- a/pyairtable/models/__init__.py +++ b/pyairtable/models/__init__.py @@ -12,10 +12,10 @@ documented separately, and none of its classes are exposed here. """ -from .audit import AuditLogEvent, AuditLogResponse -from .collaborator import Collaborator -from .comment import Comment -from .webhook import Webhook, WebhookNotification, WebhookPayload +from pyairtable.models.audit import AuditLogEvent, AuditLogResponse +from pyairtable.models.collaborator import Collaborator +from pyairtable.models.comment import Comment +from pyairtable.models.webhook import Webhook, WebhookNotification, WebhookPayload __all__ = [ "AuditLogResponse", diff --git a/pyairtable/models/_base.py b/pyairtable/models/_base.py index 0db55ffb..fefdb28a 100644 --- a/pyairtable/models/_base.py +++ b/pyairtable/models/_base.py @@ -1,6 +1,17 @@ from datetime import datetime from functools import partial -from typing import Any, ClassVar, Dict, Iterable, Mapping, Optional, Set, Type, Union +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Dict, + Iterable, + Mapping, + Optional, + Set, + Type, + Union, +) import inflection import pydantic @@ -12,6 +23,9 @@ datetime_to_iso_str, ) +if TYPE_CHECKING: + from pyairtable.api.api import Api + class AirtableModel(pydantic.BaseModel): """ @@ -46,7 +60,7 @@ def __init__(self, **data: Any) -> None: def from_api( cls, obj: Dict[str, Any], - api: "pyairtable.api.api.Api", + api: "Api", *, context: Optional[Any] = None, ) -> SelfType: @@ -73,7 +87,7 @@ def _context_name(obj: Any) -> str: def cascade_api( obj: Any, - api: "pyairtable.api.api.Api", + api: "Api", *, context: Optional[Any] = None, ) -> None: @@ -132,7 +146,7 @@ class RestfulModel(AirtableModel): __url_pattern: ClassVar[str] = "" - _api: "pyairtable.api.api.Api" = pydantic.PrivateAttr() + _api: "Api" = pydantic.PrivateAttr() _url: str = pydantic.PrivateAttr(default="") _url_context: Any = pydantic.PrivateAttr(default=None) @@ -140,7 +154,7 @@ def __init_subclass__(cls, **kwargs: Any) -> None: cls.__url_pattern = kwargs.pop("url", cls.__url_pattern) super().__init_subclass__() - def _set_api(self, api: "pyairtable.api.api.Api", context: Dict[str, Any]) -> None: + def _set_api(self, api: "Api", context: Dict[str, Any]) -> None: """ Set a link to the API and build the REST URL used for this resource. """ @@ -305,6 +319,3 @@ def rebuild_models( for value in obj.values(): if isinstance(value, type) and issubclass(value, AirtableModel): rebuild_models(value, memo=memo) - - -import pyairtable.api.api # noqa diff --git a/pyairtable/models/collaborator.py b/pyairtable/models/collaborator.py index ab0eb550..3e4e9051 100644 --- a/pyairtable/models/collaborator.py +++ b/pyairtable/models/collaborator.py @@ -2,7 +2,7 @@ from typing_extensions import TypeAlias -from ._base import AirtableModel +from pyairtable.models._base import AirtableModel UserId: TypeAlias = str diff --git a/pyairtable/models/comment.py b/pyairtable/models/comment.py index 8cd15238..2f2cc688 100644 --- a/pyairtable/models/comment.py +++ b/pyairtable/models/comment.py @@ -3,8 +3,13 @@ import pydantic -from ._base import AirtableModel, CanDeleteModel, CanUpdateModel, rebuild_models -from .collaborator import Collaborator +from pyairtable.models._base import ( + AirtableModel, + CanDeleteModel, + CanUpdateModel, + rebuild_models, +) +from pyairtable.models.collaborator import Collaborator class Comment( diff --git a/pyairtable/models/schema.py b/pyairtable/models/schema.py index c7155ba7..1cc8ca22 100644 --- a/pyairtable/models/schema.py +++ b/pyairtable/models/schema.py @@ -7,8 +7,7 @@ from typing_extensions import TypeAlias from pyairtable.api.types import AddCollaboratorDict - -from ._base import ( +from pyairtable.models._base import ( AirtableModel, CanDeleteModel, CanUpdateModel, diff --git a/pyairtable/models/webhook.py b/pyairtable/models/webhook.py index e457d2c0..833ad3c6 100644 --- a/pyairtable/models/webhook.py +++ b/pyairtable/models/webhook.py @@ -8,8 +8,7 @@ from typing_extensions import Self as SelfType from pyairtable.api.types import RecordId - -from ._base import AirtableModel, CanDeleteModel, rebuild_models +from pyairtable.models._base import AirtableModel, CanDeleteModel, rebuild_models # Shortcuts to avoid lots of line wrapping FD: Callable[[], Any] = partial(pydantic.Field, default_factory=dict) diff --git a/pyairtable/orm/__init__.py b/pyairtable/orm/__init__.py index ab7ad1d5..f6d86cd7 100644 --- a/pyairtable/orm/__init__.py +++ b/pyairtable/orm/__init__.py @@ -1,5 +1,5 @@ -from . import fields -from .model import Model, SaveResult +from pyairtable.orm import fields +from pyairtable.orm.model import Model, SaveResult __all__ = [ "Model", diff --git a/pyairtable/orm/fields.py b/pyairtable/orm/fields.py index 27e00d9e..f2ac64b9 100644 --- a/pyairtable/orm/fields.py +++ b/pyairtable/orm/fields.py @@ -50,7 +50,7 @@ from typing_extensions import Self as SelfType from typing_extensions import TypeAlias -from pyairtable import utils +from pyairtable import formulas, utils from pyairtable.api.types import ( AITextDict, AttachmentDict, @@ -69,7 +69,7 @@ from pyairtable.orm.lists import AttachmentsList, ChangeTrackingList if TYPE_CHECKING: - from pyairtable.orm import Model # noqa + from pyairtable.orm import Model _ClassInfo: TypeAlias = Union[type, Tuple["_ClassInfo", ...]] @@ -610,7 +610,7 @@ def __init__( lazy: If ``True``, this field will return empty objects with only IDs; call :meth:`~pyairtable.orm.Model.fetch` to retrieve values. """ - from pyairtable.orm import Model # noqa, avoid circular import + from pyairtable.orm import Model if not ( model is _LinkFieldOptions.LinkSelf @@ -1588,7 +1588,3 @@ class CreatedTimeField(RequiredDatetimeField): "UrlField", ] # [[[end]]] (checksum: 87b0a100c9e30523d9aab8cc935c7960) - - -# Delayed import to avoid circular dependency -from pyairtable import formulas # noqa diff --git a/tests/test_api_api.py b/tests/test_api_api.py index 6583f4b9..458497b2 100644 --- a/tests/test_api_api.py +++ b/tests/test_api_api.py @@ -2,7 +2,7 @@ import pytest -from pyairtable import Api, Base, Table # noqa +from pyairtable import Api, Base, Table @pytest.fixture