Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactored methods/properties for constructing URLs in the API #399

Merged
merged 7 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,14 @@ Changelog
- `PR #395 <https://github.com/gtalarico/pyairtable/pull/395>`_
* Dropped support for Pydantic 1.x.
- `PR #397 <https://github.com/gtalarico/pyairtable/pull/397>`_
* Refactored methods/properties for constructing URLs in the API.
- `PR #399 <https://github.com/gtalarico/pyairtable/pull/399>`_

2.3.4 (2024-10-21)
------------------------

* Fixed a crash at import time under Python 3.13.
`PR #396 <https://github.com/gtalarico/pyairtable/pull/396>`_
- `PR #396 <https://github.com/gtalarico/pyairtable/pull/396>`_

2.3.3 (2024-03-22)
------------------------
Expand Down
31 changes: 31 additions & 0 deletions docs/source/migrations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,37 @@ Deprecated metadata module removed
The 3.0 release removed the ``pyairtable.metadata`` module. For supported alternatives,
see :doc:`metadata`.

Changes to generating URLs
---------------------------------------------

The following properties and methods for constructing URLs have been renamed or removed.
These methods now return instances of :class:`~pyairtable.utils.Url`, which is a
subclass of ``str`` that has some overloaded operators. See docs for more details.

.. list-table::
:header-rows: 1

* - Building a URL in 2.x
- Building a URL in 3.0
* - ``table.url``
- ``table.urls.records``
* - ``table.record_url(record_id)``
- ``table.urls.record(record_id)``
* - ``table.meta_url("one", "two")``
- ``table.urls.meta / "one" / "two"``
* - ``table.meta_url(*parts)``
- ``table.urls.meta // parts``
* - ``base.url``
- (removed; was invalid)
* - ``base.meta_url("one", "two")``
- ``base.urls.meta / "one" / "two"``
* - ``base.webhooks_url()``
- ``base.urls.webhooks``
* - ``enterprise.url``
- ``enterprise.urls.meta``
* - ``workspace.url``
- ``workspace.urls.meta``

Changes to the formulas module
---------------------------------------------

Expand Down
28 changes: 20 additions & 8 deletions pyairtable/api/api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import posixpath
from functools import cached_property
from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple, TypeVar, Union

import requests
Expand All @@ -11,7 +11,13 @@
from pyairtable.api.types import UserAndScopesDict, assert_typed_dict
from pyairtable.api.workspace import Workspace
from pyairtable.models.schema import Bases
from pyairtable.utils import cache_unless_forced, chunked, enterprise_only
from pyairtable.utils import (
Url,
UrlBuilder,
cache_unless_forced,
chunked,
enterprise_only,
)

T = TypeVar("T")
TimeoutTuple: TypeAlias = Tuple[int, int]
Expand Down Expand Up @@ -40,10 +46,16 @@ class Api:
# Cached metadata to reduce API calls
_bases: Optional[Dict[str, "pyairtable.api.base.Base"]] = None

endpoint_url: str
endpoint_url: Url
session: Session
use_field_ids: bool

class _urls(UrlBuilder):
whoami = Url("meta/whoami")
bases = Url("meta/bases")

urls = cached_property(_urls)

def __init__(
self,
api_key: str,
Expand Down Expand Up @@ -77,7 +89,7 @@ def __init__(
else:
self.session = retrying._RetryingSession(retry_strategy)

self.endpoint_url = endpoint_url
self.endpoint_url = Url(endpoint_url)
self.timeout = timeout
self.api_key = api_key
self.use_field_ids = use_field_ids
Expand All @@ -102,7 +114,7 @@ def whoami(self) -> UserAndScopesDict:
Return the current user ID and (if connected via OAuth) the list of scopes.
See `Get user ID & scopes <https://airtable.com/developers/web/api/get-user-id-scopes>`_ for more information.
"""
data = self.request("GET", self.build_url("meta/whoami"))
data = self.request("GET", self.urls.whoami)
return assert_typed_dict(UserAndScopesDict, data)

def workspace(self, workspace_id: str) -> Workspace:
Expand Down Expand Up @@ -136,7 +148,7 @@ def _base_info(self) -> Bases:
"""
Return a schema object that represents all bases available via the API.
"""
url = self.build_url("meta/bases")
url = self.urls.bases
data = {
"bases": [
base_info
Expand Down Expand Up @@ -211,12 +223,12 @@ def table(
base = self.base(base_id, validate=validate, force=force)
return base.table(table_name, validate=validate, force=force)

def build_url(self, *components: str) -> str:
def build_url(self, *components: str) -> Url:
"""
Build a URL to the Airtable API endpoint with the given URL components,
including the API version number.
"""
return posixpath.join(self.endpoint_url, self.VERSION, *components)
return self.endpoint_url / self.VERSION // components

def request(
self,
Expand Down
58 changes: 36 additions & 22 deletions pyairtable/api/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import warnings
from functools import cached_property
from typing import Any, Dict, List, Optional, Sequence, Union

import pyairtable.api.api
Expand All @@ -10,7 +11,7 @@
Webhook,
WebhookSpecification,
)
from pyairtable.utils import cache_unless_forced, enterprise_only
from pyairtable.utils import Url, UrlBuilder, cache_unless_forced, enterprise_only


class Base:
Expand All @@ -37,6 +38,33 @@ class Base:
_schema: Optional[BaseSchema] = None
_shares: Optional[List[BaseShares.Info]] = None

class _urls(UrlBuilder):
#: URL for retrieving the base's metadata and collaborators.
meta = Url("meta/bases/{id}")

#: URL for retrieving information about the base's interfaces.
interfaces = meta / "interfaces"

#: URL for retrieving the base's shares.
shares = meta / "shares"

#: URL for retrieving the base's schema.
tables = meta / "tables"

#: URL for POST requests that modify collaborations on the base.
collaborators = meta / "collaborators"

#: URL for retrieving or modifying the base's webhooks.
webhooks = Url("bases/{id}/webhooks")

def interface(self, interface_id: str) -> Url:
"""
URL for retrieving information about a specific interface on the base.
"""
return self.interfaces / interface_id

urls = cached_property(_urls)

def __init__(
self,
api: Union["pyairtable.api.api.Api", str],
Expand Down Expand Up @@ -154,23 +182,13 @@ def create_table(
`Airtable field model <https://airtable.com/developers/web/api/field-model>`__.
description: The table description. Must be no longer than 20k characters.
"""
url = self.meta_url("tables")
url = self.urls.tables
payload = {"name": name, "fields": fields}
if description:
payload["description"] = description
response = self.api.post(url, json=payload)
return self.table(response["id"], validate=True, force=True)

@property
def url(self) -> str:
return self.api.build_url(self.id)

def meta_url(self, *components: Any) -> str:
"""
Build a URL to a metadata endpoint for this base.
"""
return self.api.build_url("meta/bases", self.id, *components)

@cache_unless_forced
def schema(self) -> BaseSchema:
"""
Expand All @@ -184,15 +202,11 @@ def schema(self) -> BaseSchema:
>>> base.schema().table("My Table")
TableSchema(id="...", name="My Table", ...)
"""
url = self.meta_url("tables")
url = self.urls.tables
params = {"include": ["visibleFieldIds"]}
data = self.api.get(url, params=params)
return BaseSchema.from_api(data, self.api, context=self)

@property
def webhooks_url(self) -> str:
return self.api.build_url("bases", self.id, "webhooks")

def webhooks(self) -> List[Webhook]:
"""
Retrieve all the base's webhooks
Expand All @@ -214,7 +228,7 @@ def webhooks(self) -> List[Webhook]:
)
]
"""
response = self.api.get(self.webhooks_url)
response = self.api.get(self.urls.webhooks)
return [
Webhook.from_api(data, self.api, context=self)
for data in response["webhooks"]
Expand Down Expand Up @@ -280,7 +294,7 @@ def add_webhook(

create = CreateWebhook(notification_url=notify_url, specification=spec)
request = create.model_dump(by_alias=True, exclude_unset=True)
response = self.api.post(self.webhooks_url, json=request)
response = self.api.post(self.urls.webhooks, json=request)
return CreateWebhookResponse.from_api(response, self.api)

@enterprise_only
Expand All @@ -290,7 +304,7 @@ def collaborators(self) -> "BaseCollaborators":
Retrieve `base collaborators <https://airtable.com/developers/web/api/get-base-collaborators>`__.
"""
params = {"include": ["collaborators", "inviteLinks", "interfaces"]}
data = self.api.get(self.meta_url(), params=params)
data = self.api.get(self.urls.meta, params=params)
return BaseCollaborators.from_api(data, self.api, context=self)

@enterprise_only
Expand All @@ -299,7 +313,7 @@ def shares(self) -> List[BaseShares.Info]:
"""
Retrieve `base shares <https://airtable.com/developers/web/api/list-shares>`__.
"""
data = self.api.get(self.meta_url("shares"))
data = self.api.get(self.urls.shares)
shares_obj = BaseShares.from_api(data, self.api, context=self)
return shares_obj.shares

Expand All @@ -312,4 +326,4 @@ def delete(self) -> None:
>>> base = api.base("appMxESAta6clCCwF")
>>> base.delete()
"""
self.api.delete(self.meta_url())
self.api.delete(self.urls.meta)
Loading
Loading