-
Notifications
You must be signed in to change notification settings - Fork 8
refactor: use static duck typing for packages #356
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,213 +1,99 @@ | ||||||
| from __future__ import annotations | ||||||
|
|
||||||
| import posixpath | ||||||
| from typing import Generator, Literal, Optional, TypedDict | ||||||
| from collections.abc import Mapping, Sized | ||||||
| from typing import Any, Literal, Protocol, SupportsIndex, overload | ||||||
|
|
||||||
| from typing_extensions import NotRequired, Required, Unpack | ||||||
|
|
||||||
| from posit.connect.context import requires | ||||||
| from posit.connect.errors import ClientError | ||||||
| from posit.connect.paginator import Paginator | ||||||
| class _ContentPackage(Mapping[str, Any]): | ||||||
| pass | ||||||
|
|
||||||
| from .resources import Active, ActiveFinderMethods, ActiveSequence | ||||||
|
|
||||||
| class _ContentPackages(Sized, Protocol): | ||||||
| @overload | ||||||
| def __getitem__(self, index: SupportsIndex) -> _ContentPackage: ... | ||||||
|
|
||||||
| class ContentPackage(Active): | ||||||
| class _Package(TypedDict): | ||||||
| language: Required[str] | ||||||
| name: Required[str] | ||||||
| version: Required[str] | ||||||
| hash: Required[Optional[str]] | ||||||
| @overload | ||||||
| def __getitem__(self, index: slice) -> _ContentPackage: ... | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Comment on lines
+12
to
+16
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to put this into a generic helper class? class _GetItem(Sized, Generic[T], Protocol):
@overload
def __getitem__(self, index: SupportsIndex) -> T: ...
@overload
def __getitem__(self, index: slice) -> list[T]: ... |
||||||
|
|
||||||
| def __init__(self, ctx, /, **attributes: Unpack[_Package]): | ||||||
| # todo - passing "" is a hack since path isn't needed. Instead, this class should inherit from Resource, but ActiveSequence is designed to operate on Active. That should change. | ||||||
| super().__init__(ctx, "", **attributes) | ||||||
|
|
||||||
|
|
||||||
| class ContentPackages(ActiveFinderMethods["ContentPackage"], ActiveSequence["ContentPackage"]): | ||||||
| """A collection of packages.""" | ||||||
|
|
||||||
| def __init__(self, ctx, path): | ||||||
| super().__init__(ctx, path, "name") | ||||||
|
|
||||||
| def _create_instance(self, path, /, **attributes): # noqa: ARG002 | ||||||
| return ContentPackage(self._ctx, **attributes) | ||||||
|
|
||||||
| def fetch(self, **conditions): | ||||||
| try: | ||||||
| return super().fetch(**conditions) | ||||||
| except ClientError as e: | ||||||
| if e.http_status == 204: | ||||||
| return [] | ||||||
| raise e | ||||||
|
|
||||||
| def find(self, uid): | ||||||
| raise NotImplementedError("The 'find' method is not support by the Packages API.") | ||||||
|
|
||||||
| class _FindBy(TypedDict, total=False): | ||||||
| language: NotRequired[Literal["python", "r"]] | ||||||
| """Programming language ecosystem, options are 'python' and 'r'""" | ||||||
|
|
||||||
| name: NotRequired[str] | ||||||
| """The package name""" | ||||||
|
|
||||||
| version: NotRequired[str] | ||||||
| """The package version""" | ||||||
|
|
||||||
| hash: NotRequired[Optional[str]] | ||||||
| """Package description hash for R packages.""" | ||||||
|
|
||||||
| def find_by(self, **conditions: Unpack[_FindBy]): # type: ignore | ||||||
| def find_by( | ||||||
| self, | ||||||
| *, | ||||||
| language: Literal["python", "r"] = ..., | ||||||
| name: str = ..., | ||||||
| version: str = ..., | ||||||
| hash: str | None = ..., # noqa: A002 | ||||||
| ) -> _ContentPackage | None: | ||||||
| """ | ||||||
| Find the first record matching the specified conditions. | ||||||
| There is no implied ordering, so if order matters, you should specify it yourself. | ||||||
| Parameters | ||||||
| ---------- | ||||||
| **conditions : Unpack[_FindBy] | ||||||
| Conditions for filtering packages. The following keys are accepted: | ||||||
| language : {"python", "r"}, not required | ||||||
| Programming language ecosystem, options are 'python' and 'r' | ||||||
| name : str, not required | ||||||
| The package name | ||||||
| version : str, not required | ||||||
| The package version | ||||||
| hash : str or None, optional, not required | ||||||
| Package description hash for R packages. | ||||||
| Returns | ||||||
| ------- | ||||||
| Optional[T] | ||||||
| _ContentPackage | None | ||||||
| The first record matching the specified conditions, or `None` if no such record exists. | ||||||
| """ | ||||||
| return super().find_by(**conditions) | ||||||
|
|
||||||
|
|
||||||
| class ContentPackagesMixin(Active): | ||||||
| """Mixin class to add a packages attribute.""" | ||||||
|
|
||||||
| @property | ||||||
| @requires(version="2024.10.0-dev") | ||||||
| def packages(self): | ||||||
| path = posixpath.join(self._path, "packages") | ||||||
| return ContentPackages(self._ctx, path) | ||||||
|
|
||||||
|
|
||||||
| class Package(Active): | ||||||
| class _Package(TypedDict): | ||||||
| language: Required[Literal["python", "r"]] | ||||||
| """Programming language ecosystem, options are 'python' and 'r'""" | ||||||
|
|
||||||
| language_version: Required[str] | ||||||
| """Programming language version""" | ||||||
|
|
||||||
| name: Required[str] | ||||||
| """The package name""" | ||||||
| ... | ||||||
|
|
||||||
| version: Required[str] | ||||||
| """The package version""" | ||||||
|
|
||||||
| hash: Required[Optional[str]] | ||||||
| """Package description hash for R packages.""" | ||||||
| class _Package(Mapping[str, Any]): | ||||||
| pass | ||||||
|
|
||||||
| bundle_id: Required[str] | ||||||
| """The unique identifier of the bundle this package is associated with""" | ||||||
|
|
||||||
| app_id: Required[str] | ||||||
| """The numerical identifier of the application this package is associated with""" | ||||||
| class _Packages(Sized, Protocol): | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to suffix the class names with
Suggested change
(Much easier to perform with "rename symbol")
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, I think we should make these public types. External users may want to use them for type hinting. For example... from posit.connect import Client, Packages
client = Client()
packages: Packages = client.packages |
||||||
| @overload | ||||||
| def __getitem__(self, index: SupportsIndex) -> _ContentPackage: ... | ||||||
|
|
||||||
| app_guid: Required[str] | ||||||
| """The unique identifier of the application this package is associated with""" | ||||||
| @overload | ||||||
| def __getitem__(self, index: slice) -> _ContentPackage: ... | ||||||
|
|
||||||
| def __init__(self, ctx, /, **attributes: Unpack[_Package]): | ||||||
| # todo - passing "" is a hack since path isn't needed. Instead, this class should inherit from Resource, but ActiveSequence is designed to operate on Active. That should change. | ||||||
| super().__init__(ctx, "", **attributes) | ||||||
|
|
||||||
|
|
||||||
| class Packages(ActiveFinderMethods["Package"], ActiveSequence["Package"]): | ||||||
| def __init__(self, ctx, path): | ||||||
| super().__init__(ctx, path, "name") | ||||||
|
|
||||||
| def _create_instance(self, path, /, **attributes): # noqa: ARG002 | ||||||
| return Package(self._ctx, **attributes) | ||||||
|
|
||||||
| class _Fetch(TypedDict, total=False): | ||||||
| language: Required[Literal["python", "r"]] | ||||||
| """Programming language ecosystem, options are 'python' and 'r'""" | ||||||
|
|
||||||
| name: Required[str] | ||||||
| """The package name""" | ||||||
|
|
||||||
| version: Required[str] | ||||||
| """The package version""" | ||||||
|
|
||||||
| def fetch(self, **conditions: Unpack[_Fetch]) -> Generator["Package"]: # type: ignore | ||||||
| # todo - add pagination support to ActiveSequence | ||||||
| url = self._ctx.url + self._path | ||||||
| paginator = Paginator(self._ctx.session, url, dict(**conditions)) | ||||||
| for page in paginator.fetch_pages(): | ||||||
| results = page.results | ||||||
| yield from (self._create_instance("", **result) for result in results) | ||||||
|
|
||||||
| def find(self, uid): | ||||||
| raise NotImplementedError("The 'find' method is not support by the Packages API.") | ||||||
|
|
||||||
| class _FindBy(TypedDict, total=False): | ||||||
| language: NotRequired[Literal["python", "r"]] | ||||||
| """Programming language ecosystem, options are 'python' and 'r'""" | ||||||
|
|
||||||
| language_version: NotRequired[str] | ||||||
| """Programming language version""" | ||||||
|
|
||||||
| name: NotRequired[str] | ||||||
| """The package name""" | ||||||
|
|
||||||
| version: NotRequired[str] | ||||||
| """The package version""" | ||||||
|
|
||||||
| hash: NotRequired[Optional[str]] | ||||||
| """Package description hash for R packages.""" | ||||||
|
|
||||||
| bundle_id: NotRequired[str] | ||||||
| """The unique identifier of the bundle this package is associated with""" | ||||||
|
|
||||||
| app_id: NotRequired[str] | ||||||
| """The numerical identifier of the application this package is associated with""" | ||||||
|
|
||||||
| app_guid: NotRequired[str] | ||||||
| """The unique identifier of the application this package is associated with""" | ||||||
|
|
||||||
| def find_by(self, **conditions: Unpack[_FindBy]) -> "Package | None": # type: ignore | ||||||
| def find_by( | ||||||
| self, | ||||||
| *, | ||||||
| language: Literal["python", "r"] = ..., | ||||||
| name: str = ..., | ||||||
| version: str = ..., | ||||||
| hash: str | None = ..., # noqa: A002, | ||||||
| bundle_id: str = ..., | ||||||
| app_id: str = ..., | ||||||
| app_guid: str = ..., | ||||||
| ) -> _ContentPackage | None: | ||||||
| """ | ||||||
| Find the first record matching the specified conditions. | ||||||
| There is no implied ordering, so if order matters, you should specify it yourself. | ||||||
| Parameters | ||||||
| ---------- | ||||||
| **conditions : Unpack[_FindBy] | ||||||
| Conditions for filtering packages. The following keys are accepted: | ||||||
| language : {"python", "r"}, not required | ||||||
| Programming language ecosystem, options are 'python' and 'r' | ||||||
| name : str, not required | ||||||
| The package name | ||||||
| version : str, not required | ||||||
| The package version | ||||||
| hash : str or None, optional, not required | ||||||
| Package description hash for R packages. | ||||||
| bundle_id: str, not required | ||||||
| The unique identifier of the bundle this package is associated with. | ||||||
| app_id: str, not required | ||||||
| The numerical identifier of the application this package is associated with. | ||||||
| app_guid: str, not required | ||||||
| The unique identifier of the application this package is associated with. | ||||||
| Returns | ||||||
| ------- | ||||||
| Optional[Package] | ||||||
| _Package | None | ||||||
| The first record matching the specified conditions, or `None` if no such record exists. | ||||||
| """ | ||||||
| return super().find_by(**conditions) | ||||||
| ... | ||||||
Uh oh!
There was an error while loading. Please reload this page.