From 8eb048617cef30c4d282db3a702b6e28ead9a4f9 Mon Sep 17 00:00:00 2001 From: tdstein Date: Mon, 16 Dec 2024 14:46:12 -0500 Subject: [PATCH 1/8] add base protocols with correct method signatures --- src/posit/connect/bundles.py | 4 +-- src/posit/connect/content.py | 10 +++--- src/posit/connect/environments.py | 19 +++-------- src/posit/connect/groups.py | 4 +-- src/posit/connect/jobs.py | 19 +++-------- src/posit/connect/metrics/shiny_usage.py | 4 +-- src/posit/connect/metrics/usage.py | 2 +- src/posit/connect/metrics/visits.py | 4 +-- src/posit/connect/oauth/associations.py | 4 +-- src/posit/connect/oauth/integrations.py | 4 +-- src/posit/connect/oauth/sessions.py | 4 +-- src/posit/connect/packages.py | 27 ++++------------ src/posit/connect/permissions.py | 4 +-- src/posit/connect/resources.py | 40 +++++++++++++++++++++--- src/posit/connect/tasks.py | 2 +- src/posit/connect/users.py | 4 +-- src/posit/connect/vanities.py | 6 ++-- src/posit/connect/variants.py | 4 +-- tests/posit/connect/test_resources.py | 4 +-- 19 files changed, 82 insertions(+), 87 deletions(-) diff --git a/src/posit/connect/bundles.py b/src/posit/connect/bundles.py index 515751fa..3363e3bb 100644 --- a/src/posit/connect/bundles.py +++ b/src/posit/connect/bundles.py @@ -11,11 +11,11 @@ from .context import Context -class BundleMetadata(resources.Resource): +class BundleMetadata(resources.BaseResource): pass -class Bundle(resources.Resource): +class Bundle(resources.BaseResource): @property def metadata(self) -> BundleMetadata: return BundleMetadata(self._ctx, **self.get("metadata", {})) diff --git a/src/posit/connect/content.py b/src/posit/connect/content.py index a98e9a64..dd900f6d 100644 --- a/src/posit/connect/content.py +++ b/src/posit/connect/content.py @@ -24,7 +24,7 @@ from .errors import ClientError from .oauth.associations import ContentItemAssociations from .permissions import Permissions -from .resources import Active, Resource, Resources, _ResourceSequence +from .resources import Active, BaseResource, Resources, _ResourceSequence from .tags import ContentItemTags from .vanities import VanityMixin from .variants import Variants @@ -160,7 +160,7 @@ def update( ) -class ContentItemOAuth(Resource): +class ContentItemOAuth(BaseResource): def __init__(self, ctx: Context, content_guid: str) -> None: super().__init__(ctx) self["content_guid"] = content_guid @@ -170,11 +170,11 @@ def associations(self) -> ContentItemAssociations: return ContentItemAssociations(self._ctx, content_guid=self["content_guid"]) -class ContentItemOwner(Resource): +class ContentItemOwner(BaseResource): pass -class ContentItem(Active, VanityMixin, Resource): +class ContentItem(Active, VanityMixin, BaseResource): class _AttrsBase(TypedDict, total=False): # # `name` will be set by other _Attrs classes # name: str @@ -376,7 +376,7 @@ def restart(self) -> None: f"Restart not supported for this application mode: {self['app_mode']}. Did you need to use the 'render()' method instead? Note that some application modes do not support 'render()' or 'restart()'.", ) - def update( + def update( # type: ignore[reportIncompatibleMethodOverride] self, **attrs: Unpack[ContentItem._Attrs], ) -> None: diff --git a/src/posit/connect/environments.py b/src/posit/connect/environments.py index 4b229fa9..d2cc85e5 100644 --- a/src/posit/connect/environments.py +++ b/src/posit/connect/environments.py @@ -1,19 +1,17 @@ from __future__ import annotations from abc import abstractmethod -from collections.abc import Mapping, Sized +from typing import Protocol from typing_extensions import ( - Any, List, Literal, - Protocol, - SupportsIndex, TypedDict, - overload, runtime_checkable, ) +from .resources import Resource, ResourceSequence + MatchingType = Literal["any", "exact", "none"] """Directions for how environments are considered for selection. @@ -40,7 +38,7 @@ class Installations(TypedDict): """Interpreter installations in an execution environment.""" -class Environment(Mapping[str, Any]): +class Environment(Resource): @abstractmethod def destroy(self) -> None: """Destroy the environment. @@ -95,13 +93,7 @@ def update( @runtime_checkable -class Environments(Sized, Protocol): - @overload - def __getitem__(self, index: SupportsIndex) -> Environment: ... - - @overload - def __getitem__(self, index: slice) -> List[Environment]: ... - +class Environments(ResourceSequence[Environment], Protocol): def create( self, *, @@ -217,4 +209,3 @@ def find_by( ---- This action requires administrator or publisher privileges. """ - ... diff --git a/src/posit/connect/groups.py b/src/posit/connect/groups.py index 717b18d9..fa5cf9dd 100644 --- a/src/posit/connect/groups.py +++ b/src/posit/connect/groups.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, List, Optional, overload from .paginator import Paginator -from .resources import Resource, Resources +from .resources import BaseResource, Resources if TYPE_CHECKING: import requests @@ -14,7 +14,7 @@ from .users import User -class Group(Resource): +class Group(BaseResource): def __init__(self, ctx: Context, **kwargs) -> None: super().__init__(ctx, **kwargs) self._ctx: Context = ctx diff --git a/src/posit/connect/jobs.py b/src/posit/connect/jobs.py index d9124f82..8797686b 100644 --- a/src/posit/connect/jobs.py +++ b/src/posit/connect/jobs.py @@ -1,18 +1,14 @@ from __future__ import annotations -from abc import abstractmethod -from collections.abc import Mapping, Sized from typing import ( - Any, Iterable, - List, Literal, Protocol, - SupportsIndex, - overload, runtime_checkable, ) +from .resources import Resource, ResourceSequence + JobTag = Literal[ "unknown", "build_report", @@ -43,8 +39,7 @@ StatusCode = Literal[0, 1, 2] -class Job(Mapping[str, Any]): - @abstractmethod +class Job(Resource, Protocol): def destroy(self) -> None: """Destroy the job. @@ -59,13 +54,7 @@ def destroy(self) -> None: @runtime_checkable -class Jobs(Sized, Protocol): - @overload - def __getitem__(self, index: SupportsIndex) -> Job: ... - - @overload - def __getitem__(self, index: slice) -> List[Job]: ... - +class Jobs(ResourceSequence[Job], Protocol): def fetch(self) -> Iterable[Job]: """Fetch all jobs. diff --git a/src/posit/connect/metrics/shiny_usage.py b/src/posit/connect/metrics/shiny_usage.py index 68a391be..a5240f4f 100644 --- a/src/posit/connect/metrics/shiny_usage.py +++ b/src/posit/connect/metrics/shiny_usage.py @@ -3,10 +3,10 @@ from typing import List, overload from ..cursors import CursorPaginator -from ..resources import Resource, Resources +from ..resources import BaseResource, Resources -class ShinyUsageEvent(Resource): +class ShinyUsageEvent(BaseResource): @property def content_guid(self) -> str: """The associated unique content identifier. diff --git a/src/posit/connect/metrics/usage.py b/src/posit/connect/metrics/usage.py index 6714cca7..b9eac649 100644 --- a/src/posit/connect/metrics/usage.py +++ b/src/posit/connect/metrics/usage.py @@ -10,7 +10,7 @@ from . import shiny_usage, visits -class UsageEvent(resources.Resource): +class UsageEvent(resources.BaseResource): @staticmethod def from_event( event: visits.VisitEvent | shiny_usage.ShinyUsageEvent, diff --git a/src/posit/connect/metrics/visits.py b/src/posit/connect/metrics/visits.py index 393aae36..ea88dfb6 100644 --- a/src/posit/connect/metrics/visits.py +++ b/src/posit/connect/metrics/visits.py @@ -3,10 +3,10 @@ from typing import List, overload from ..cursors import CursorPaginator -from ..resources import Resource, Resources +from ..resources import BaseResource, Resources -class VisitEvent(Resource): +class VisitEvent(BaseResource): @property def content_guid(self) -> str: """The associated unique content identifier. diff --git a/src/posit/connect/oauth/associations.py b/src/posit/connect/oauth/associations.py index 9fc999e6..44723dfc 100644 --- a/src/posit/connect/oauth/associations.py +++ b/src/posit/connect/oauth/associations.py @@ -3,10 +3,10 @@ from typing import List from ..context import Context -from ..resources import Resource, Resources +from ..resources import BaseResource, Resources -class Association(Resource): +class Association(BaseResource): pass diff --git a/src/posit/connect/oauth/integrations.py b/src/posit/connect/oauth/integrations.py index 540cd6f0..3e3259e9 100644 --- a/src/posit/connect/oauth/integrations.py +++ b/src/posit/connect/oauth/integrations.py @@ -2,11 +2,11 @@ from typing import List, Optional, overload -from ..resources import Resource, Resources +from ..resources import BaseResource, Resources from .associations import IntegrationAssociations -class Integration(Resource): +class Integration(BaseResource): """OAuth integration resource.""" @property diff --git a/src/posit/connect/oauth/sessions.py b/src/posit/connect/oauth/sessions.py index 503ea254..fae3981b 100644 --- a/src/posit/connect/oauth/sessions.py +++ b/src/posit/connect/oauth/sessions.py @@ -2,10 +2,10 @@ from typing import List, Optional, overload -from ..resources import Resource, Resources +from ..resources import BaseResource, Resources -class Session(Resource): +class Session(BaseResource): """OAuth session resource.""" def delete(self) -> None: diff --git a/src/posit/connect/packages.py b/src/posit/connect/packages.py index 63289c4f..4983eaff 100644 --- a/src/posit/connect/packages.py +++ b/src/posit/connect/packages.py @@ -1,28 +1,19 @@ from __future__ import annotations from typing_extensions import ( - Any, Iterable, Literal, - Mapping, Protocol, - Sized, - SupportsIndex, - overload, ) - -class ContentPackage(Mapping[str, Any]): - pass +from .resources import Resource, ResourceSequence -class ContentPackages(Sized, Protocol): - @overload - def __getitem__(self, index: SupportsIndex) -> ContentPackage: ... +class ContentPackage(Resource, Protocol): + pass - @overload - def __getitem__(self, index: slice) -> ContentPackage: ... +class ContentPackages(ResourceSequence[ContentPackage], Protocol): def fetch( self, *, @@ -84,17 +75,11 @@ def find_by( ... -class Package(Mapping[str, Any]): +class Package(Resource, Protocol): pass -class Packages(Sized, Protocol): - @overload - def __getitem__(self, index: SupportsIndex) -> ContentPackage: ... - - @overload - def __getitem__(self, index: slice) -> ContentPackage: ... - +class Packages(ResourceSequence[Package], Protocol): def fetch( self, *, diff --git a/src/posit/connect/permissions.py b/src/posit/connect/permissions.py index e8afd9c1..211decc2 100644 --- a/src/posit/connect/permissions.py +++ b/src/posit/connect/permissions.py @@ -6,7 +6,7 @@ from requests.sessions import Session as Session -from .resources import Resource, Resources +from .resources import BaseResource, Resources if TYPE_CHECKING: from .context import Context @@ -14,7 +14,7 @@ from .users import User -class Permission(Resource): +class Permission(BaseResource): def destroy(self) -> None: """Destroy the permission.""" path = f"v1/content/{self['content_guid']}/permissions/{self['id']}" diff --git a/src/posit/connect/resources.py b/src/posit/connect/resources.py index 66e6058f..70cffc53 100644 --- a/src/posit/connect/resources.py +++ b/src/posit/connect/resources.py @@ -6,8 +6,15 @@ from typing import ( TYPE_CHECKING, Any, + Hashable, Iterable, + Iterator, + List, + Protocol, Sequence, + SupportsIndex, + TypeVar, + overload, ) from .context import Context @@ -17,7 +24,7 @@ from .context import Context -class Resource(dict): +class BaseResource(dict): def __init__(self, ctx: Context, /, **kwargs): super().__init__(**kwargs) self._ctx = ctx @@ -42,7 +49,7 @@ def __init__(self, ctx: Context) -> None: self._ctx = ctx -class Active(ABC, Resource): +class Active(ABC, BaseResource): def __init__(self, ctx: Context, path: str, /, **attributes): """A dict abstraction for any HTTP endpoint that returns a singular resource. @@ -62,7 +69,11 @@ def __init__(self, ctx: Context, path: str, /, **attributes): self._path = path -class _Resource(dict): +class Resource(Protocol): + def __getitem__(self, key: Hashable) -> Any: ... + + +class _Resource(dict, Resource): def __init__(self, ctx: Context, path: str, **attributes): self._ctx = ctx self._path = path @@ -77,7 +88,26 @@ def update(self, **attributes): # type: ignore[reportIncompatibleMethodOverride super().update(**result) -class _ResourceSequence(Sequence): +T = TypeVar("T", bound=Resource) + + +class ResourceSequence(Protocol[T]): + @overload + def __getitem__(self, index: SupportsIndex, /) -> T: ... + + @overload + def __getitem__(self, index: slice, /) -> List[T]: ... + + def __len__(self) -> int: ... + + def __iter__(self) -> Iterator[T]: ... + + def __str__(self) -> str: ... + + def __repr__(self) -> str: ... + + +class _ResourceSequence(Sequence[T], ResourceSequence[T]): def __init__(self, ctx: Context, path: str, *, uid: str = "guid"): self._ctx = ctx self._path = path @@ -89,7 +119,7 @@ def __getitem__(self, index): def __len__(self) -> int: return len(list(self.fetch())) - def __iter__(self): + def __iter__(self) -> Iterator[T]: return iter(self.fetch()) def __str__(self) -> str: diff --git a/src/posit/connect/tasks.py b/src/posit/connect/tasks.py index 907a458f..8360304d 100644 --- a/src/posit/connect/tasks.py +++ b/src/posit/connect/tasks.py @@ -7,7 +7,7 @@ from . import resources -class Task(resources.Resource): +class Task(resources.BaseResource): @property def is_finished(self) -> bool: """The task state. diff --git a/src/posit/connect/users.py b/src/posit/connect/users.py index f002cf88..334617a5 100644 --- a/src/posit/connect/users.py +++ b/src/posit/connect/users.py @@ -9,14 +9,14 @@ from . import me from .content import Content from .paginator import Paginator -from .resources import Resource, Resources +from .resources import BaseResource, Resources if TYPE_CHECKING: from .context import Context from .groups import Group -class User(Resource): +class User(BaseResource): @property def content(self) -> Content: return Content(self._ctx, owner_guid=self["guid"]) diff --git a/src/posit/connect/vanities.py b/src/posit/connect/vanities.py index 75becc6f..5a483054 100644 --- a/src/posit/connect/vanities.py +++ b/src/posit/connect/vanities.py @@ -4,10 +4,10 @@ from .context import Context from .errors import ClientError -from .resources import Resource, Resources +from .resources import BaseResource, Resources -class Vanity(Resource): +class Vanity(BaseResource): """A vanity resource. Vanities maintain custom URL paths assigned to content. @@ -115,7 +115,7 @@ def all(self) -> List[Vanity]: return [Vanity(self._ctx, **result) for result in results] -class VanityMixin(Resource): +class VanityMixin(BaseResource): """Mixin class to add a vanity attribute to a resource.""" class HasGuid(TypedDict): diff --git a/src/posit/connect/variants.py b/src/posit/connect/variants.py index ba8597c1..9970c70e 100644 --- a/src/posit/connect/variants.py +++ b/src/posit/connect/variants.py @@ -1,11 +1,11 @@ from typing import List from .context import Context -from .resources import Resource, Resources +from .resources import BaseResource, Resources from .tasks import Task -class Variant(Resource): +class Variant(BaseResource): def render(self) -> Task: path = f"variants/{self['id']}/render" response = self._ctx.client.post(path) diff --git a/tests/posit/connect/test_resources.py b/tests/posit/connect/test_resources.py index 28cf6e0e..a17d252b 100644 --- a/tests/posit/connect/test_resources.py +++ b/tests/posit/connect/test_resources.py @@ -3,13 +3,13 @@ from unittest import mock from unittest.mock import Mock -from posit.connect.resources import Resource +from posit.connect.resources import BaseResource config = Mock() session = Mock() -class FakeResource(Resource): +class FakeResource(BaseResource): @property def foo(self) -> Optional[str]: return self.get("foo") From 986e745c21f60e80cd5116269884b02d1de03873 Mon Sep 17 00:00:00 2001 From: tdstein Date: Mon, 16 Dec 2024 15:19:37 -0500 Subject: [PATCH 2/8] fix: import from typing_extensions instead of typing --- examples/connect/databricks/fastapi/app.py | 2 +- .../posit/connect/test_content_item_permissions.py | 3 +-- src/posit/connect/_api.py | 3 ++- src/posit/connect/_api_call.py | 3 ++- src/posit/connect/_json.py | 2 +- src/posit/connect/_utils.py | 2 +- src/posit/connect/bundles.py | 3 ++- src/posit/connect/client.py | 3 +-- src/posit/connect/config.py | 3 ++- src/posit/connect/content.py | 9 ++++++--- src/posit/connect/context.py | 2 +- src/posit/connect/cursors.py | 3 ++- src/posit/connect/env.py | 2 +- src/posit/connect/environments.py | 2 +- src/posit/connect/errors.py | 3 ++- src/posit/connect/external/databricks.py | 2 +- src/posit/connect/external/snowflake.py | 2 +- src/posit/connect/groups.py | 2 +- src/posit/connect/jobs.py | 2 +- src/posit/connect/metrics/shiny_usage.py | 2 +- src/posit/connect/metrics/usage.py | 3 +-- src/posit/connect/metrics/visits.py | 2 +- src/posit/connect/oauth/associations.py | 2 +- src/posit/connect/oauth/integrations.py | 2 +- src/posit/connect/oauth/oauth.py | 3 +-- src/posit/connect/oauth/sessions.py | 2 +- src/posit/connect/paginator.py | 3 ++- src/posit/connect/permissions.py | 3 +-- src/posit/connect/resources.py | 3 ++- src/posit/connect/system.py | 4 +--- src/posit/connect/tags.py | 3 +-- src/posit/connect/tasks.py | 2 +- src/posit/connect/users.py | 12 +++++++++--- src/posit/connect/vanities.py | 4 +--- src/posit/connect/variants.py | 2 +- tests/posit/connect/external/test_databricks.py | 2 +- tests/posit/connect/test_jobs.py | 3 +-- tests/posit/connect/test_resources.py | 3 ++- 38 files changed, 60 insertions(+), 53 deletions(-) diff --git a/examples/connect/databricks/fastapi/app.py b/examples/connect/databricks/fastapi/app.py index ed01f41c..a3e49e6a 100644 --- a/examples/connect/databricks/fastapi/app.py +++ b/examples/connect/databricks/fastapi/app.py @@ -3,11 +3,11 @@ from __future__ import annotations import os -from typing import TYPE_CHECKING, Annotated from databricks import sql from databricks.sdk.core import Config, databricks_cli from fastapi import FastAPI, Header +from typing_extensions import TYPE_CHECKING, Annotated from posit.connect.external.databricks import PositCredentialsStrategy diff --git a/integration/tests/posit/connect/test_content_item_permissions.py b/integration/tests/posit/connect/test_content_item_permissions.py index 518178d5..9f3ee04b 100644 --- a/integration/tests/posit/connect/test_content_item_permissions.py +++ b/integration/tests/posit/connect/test_content_item_permissions.py @@ -1,8 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import pytest +from typing_extensions import TYPE_CHECKING from posit import connect diff --git a/src/posit/connect/_api.py b/src/posit/connect/_api.py index 29325158..f45dc0ee 100644 --- a/src/posit/connect/_api.py +++ b/src/posit/connect/_api.py @@ -4,7 +4,8 @@ from __future__ import annotations from collections.abc import Mapping -from typing import TYPE_CHECKING, Any, Optional, cast + +from typing_extensions import TYPE_CHECKING, Any, Optional, cast from ._api_call import ApiCallMixin, get_api from ._json import Jsonifiable, JsonifiableDict, ResponseAttrs diff --git a/src/posit/connect/_api_call.py b/src/posit/connect/_api_call.py index 56ce70f6..0de5f0ba 100644 --- a/src/posit/connect/_api_call.py +++ b/src/posit/connect/_api_call.py @@ -1,7 +1,8 @@ from __future__ import annotations import posixpath -from typing import TYPE_CHECKING, Protocol + +from typing_extensions import TYPE_CHECKING, Protocol if TYPE_CHECKING: from ._json import Jsonifiable diff --git a/src/posit/connect/_json.py b/src/posit/connect/_json.py index 62f28f82..85f331fa 100644 --- a/src/posit/connect/_json.py +++ b/src/posit/connect/_json.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Tuple, TypeVar, Union +from typing_extensions import Dict, List, Tuple, TypeVar, Union # Implemented in https://github.com/posit-dev/py-shiny/blob/415ced034e6c500adda524abb7579731c32088b5/shiny/types.py#L357-L386 # Table from: https://github.com/python/cpython/blob/df1eec3dae3b1eddff819fd70f58b03b3fbd0eda/Lib/json/encoder.py#L77-L95 diff --git a/src/posit/connect/_utils.py b/src/posit/connect/_utils.py index 26842bbc..e279f3b7 100644 --- a/src/posit/connect/_utils.py +++ b/src/posit/connect/_utils.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any +from typing_extensions import Any def drop_none(x: dict[str, Any]) -> dict[str, Any]: diff --git a/src/posit/connect/bundles.py b/src/posit/connect/bundles.py index 3363e3bb..5dba4ee4 100644 --- a/src/posit/connect/bundles.py +++ b/src/posit/connect/bundles.py @@ -3,7 +3,8 @@ from __future__ import annotations import io -from typing import TYPE_CHECKING, List + +from typing_extensions import TYPE_CHECKING, List from . import resources, tasks diff --git a/src/posit/connect/client.py b/src/posit/connect/client.py index f0f5c3a1..6ef22c21 100644 --- a/src/posit/connect/client.py +++ b/src/posit/connect/client.py @@ -2,9 +2,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, overload - from requests import Response, Session +from typing_extensions import TYPE_CHECKING, overload from . import hooks, me from .auth import Auth diff --git a/src/posit/connect/config.py b/src/posit/connect/config.py index 59d03c87..500bda73 100644 --- a/src/posit/connect/config.py +++ b/src/posit/connect/config.py @@ -1,7 +1,8 @@ """Client configuration.""" import os -from typing import Optional + +from typing_extensions import Optional from . import urls diff --git a/src/posit/connect/content.py b/src/posit/connect/content.py index dd900f6d..f7a3fdb8 100644 --- a/src/posit/connect/content.py +++ b/src/posit/connect/content.py @@ -4,18 +4,21 @@ import posixpath import time -from typing import ( + +from typing_extensions import ( TYPE_CHECKING, Any, List, Literal, + NotRequired, Optional, + Required, + TypedDict, + Unpack, cast, overload, ) -from typing_extensions import NotRequired, Required, TypedDict, Unpack - from . import tasks from ._api import ApiDictEndpoint, JsonifiableDict from .bundles import Bundles diff --git a/src/posit/connect/context.py b/src/posit/connect/context.py index ec7ff55a..dbc456b7 100644 --- a/src/posit/connect/context.py +++ b/src/posit/connect/context.py @@ -2,9 +2,9 @@ import functools import weakref -from typing import TYPE_CHECKING, Protocol from packaging.version import Version +from typing_extensions import TYPE_CHECKING, Protocol if TYPE_CHECKING: from .client import Client diff --git a/src/posit/connect/cursors.py b/src/posit/connect/cursors.py index 22f406e1..414d965d 100644 --- a/src/posit/connect/cursors.py +++ b/src/posit/connect/cursors.py @@ -1,7 +1,8 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Generator, List + +from typing_extensions import TYPE_CHECKING, Any, Generator, List if TYPE_CHECKING: from .context import Context diff --git a/src/posit/connect/env.py b/src/posit/connect/env.py index 5ca26cd2..f801dc00 100644 --- a/src/posit/connect/env.py +++ b/src/posit/connect/env.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Iterator, List, Mapping, MutableMapping, Optional +from typing_extensions import TYPE_CHECKING, Any, Iterator, List, Mapping, MutableMapping, Optional from .resources import Resources diff --git a/src/posit/connect/environments.py b/src/posit/connect/environments.py index d2cc85e5..1fb78b59 100644 --- a/src/posit/connect/environments.py +++ b/src/posit/connect/environments.py @@ -1,11 +1,11 @@ from __future__ import annotations from abc import abstractmethod -from typing import Protocol from typing_extensions import ( List, Literal, + Protocol, TypedDict, runtime_checkable, ) diff --git a/src/posit/connect/errors.py b/src/posit/connect/errors.py index 8e8bf8db..e7ef7997 100644 --- a/src/posit/connect/errors.py +++ b/src/posit/connect/errors.py @@ -1,5 +1,6 @@ import json -from typing import Any + +from typing_extensions import Any class ClientError(Exception): diff --git a/src/posit/connect/external/databricks.py b/src/posit/connect/external/databricks.py index e73c5f71..0c9ad886 100644 --- a/src/posit/connect/external/databricks.py +++ b/src/posit/connect/external/databricks.py @@ -6,9 +6,9 @@ """ import abc -from typing import Callable, Dict, Optional import requests +from typing_extensions import Callable, Dict, Optional from ..client import Client from ..oauth import Credentials diff --git a/src/posit/connect/external/snowflake.py b/src/posit/connect/external/snowflake.py index 7f5ec923..1bc5dd74 100644 --- a/src/posit/connect/external/snowflake.py +++ b/src/posit/connect/external/snowflake.py @@ -3,7 +3,7 @@ NOTE: The APIs in this module are provided as a convenience and are subject to breaking changes. """ -from typing import Optional +from typing_extensions import Optional from ..client import Client from .external import is_local diff --git a/src/posit/connect/groups.py b/src/posit/connect/groups.py index fa5cf9dd..36f530e4 100644 --- a/src/posit/connect/groups.py +++ b/src/posit/connect/groups.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List, Optional, overload +from typing_extensions import TYPE_CHECKING, List, Optional, overload from .paginator import Paginator from .resources import BaseResource, Resources diff --git a/src/posit/connect/jobs.py b/src/posit/connect/jobs.py index 8797686b..959c1dab 100644 --- a/src/posit/connect/jobs.py +++ b/src/posit/connect/jobs.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import ( +from typing_extensions import ( Iterable, Literal, Protocol, diff --git a/src/posit/connect/metrics/shiny_usage.py b/src/posit/connect/metrics/shiny_usage.py index a5240f4f..d9657d9f 100644 --- a/src/posit/connect/metrics/shiny_usage.py +++ b/src/posit/connect/metrics/shiny_usage.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import List, overload +from typing_extensions import List, overload from ..cursors import CursorPaginator from ..resources import BaseResource, Resources diff --git a/src/posit/connect/metrics/usage.py b/src/posit/connect/metrics/usage.py index b9eac649..b513a52f 100644 --- a/src/posit/connect/metrics/usage.py +++ b/src/posit/connect/metrics/usage.py @@ -2,9 +2,8 @@ from __future__ import annotations -from typing import List, overload - from requests.sessions import Session as Session +from typing_extensions import List, overload from .. import resources from . import shiny_usage, visits diff --git a/src/posit/connect/metrics/visits.py b/src/posit/connect/metrics/visits.py index ea88dfb6..503d14af 100644 --- a/src/posit/connect/metrics/visits.py +++ b/src/posit/connect/metrics/visits.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import List, overload +from typing_extensions import List, overload from ..cursors import CursorPaginator from ..resources import BaseResource, Resources diff --git a/src/posit/connect/oauth/associations.py b/src/posit/connect/oauth/associations.py index 44723dfc..5d20db02 100644 --- a/src/posit/connect/oauth/associations.py +++ b/src/posit/connect/oauth/associations.py @@ -1,6 +1,6 @@ """OAuth association resources.""" -from typing import List +from typing_extensions import List from ..context import Context from ..resources import BaseResource, Resources diff --git a/src/posit/connect/oauth/integrations.py b/src/posit/connect/oauth/integrations.py index 3e3259e9..7dca39eb 100644 --- a/src/posit/connect/oauth/integrations.py +++ b/src/posit/connect/oauth/integrations.py @@ -1,6 +1,6 @@ """OAuth integration resources.""" -from typing import List, Optional, overload +from typing_extensions import List, Optional, overload from ..resources import BaseResource, Resources from .associations import IntegrationAssociations diff --git a/src/posit/connect/oauth/oauth.py b/src/posit/connect/oauth/oauth.py index 90df7270..efec4395 100644 --- a/src/posit/connect/oauth/oauth.py +++ b/src/posit/connect/oauth/oauth.py @@ -1,9 +1,8 @@ from __future__ import annotations import os -from typing import TYPE_CHECKING, Optional -from typing_extensions import TypedDict +from typing_extensions import TYPE_CHECKING, Optional, TypedDict from ..resources import Resources from .integrations import Integrations diff --git a/src/posit/connect/oauth/sessions.py b/src/posit/connect/oauth/sessions.py index fae3981b..e0db5829 100644 --- a/src/posit/connect/oauth/sessions.py +++ b/src/posit/connect/oauth/sessions.py @@ -1,6 +1,6 @@ """OAuth session resources.""" -from typing import List, Optional, overload +from typing_extensions import List, Optional, overload from ..resources import BaseResource, Resources diff --git a/src/posit/connect/paginator.py b/src/posit/connect/paginator.py index 75b224d3..0252e66f 100644 --- a/src/posit/connect/paginator.py +++ b/src/posit/connect/paginator.py @@ -1,7 +1,8 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING, Generator, List + +from typing_extensions import TYPE_CHECKING, Generator, List if TYPE_CHECKING: from .context import Context diff --git a/src/posit/connect/permissions.py b/src/posit/connect/permissions.py index 211decc2..4b029cb5 100644 --- a/src/posit/connect/permissions.py +++ b/src/posit/connect/permissions.py @@ -2,9 +2,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List, Optional, overload - from requests.sessions import Session as Session +from typing_extensions import TYPE_CHECKING, List, Optional, overload from .resources import BaseResource, Resources diff --git a/src/posit/connect/resources.py b/src/posit/connect/resources.py index 70cffc53..7095c8fa 100644 --- a/src/posit/connect/resources.py +++ b/src/posit/connect/resources.py @@ -3,7 +3,8 @@ import posixpath import warnings from abc import ABC -from typing import ( + +from typing_extensions import ( TYPE_CHECKING, Any, Hashable, diff --git a/src/posit/connect/system.py b/src/posit/connect/system.py index 446eff4f..46353fd5 100644 --- a/src/posit/connect/system.py +++ b/src/posit/connect/system.py @@ -2,9 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List, Literal, overload - -from typing_extensions import TypedDict, Unpack +from typing_extensions import TYPE_CHECKING, List, Literal, TypedDict, Unpack, overload from .context import ContextManager from .resources import Active diff --git a/src/posit/connect/tags.py b/src/posit/connect/tags.py index 502a7b49..0ff303ae 100644 --- a/src/posit/connect/tags.py +++ b/src/posit/connect/tags.py @@ -1,9 +1,8 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Optional, overload -from typing_extensions import NotRequired, TypedDict, Unpack +from typing_extensions import TYPE_CHECKING, NotRequired, Optional, TypedDict, Unpack, overload from .context import Context, ContextManager from .resources import Active diff --git a/src/posit/connect/tasks.py b/src/posit/connect/tasks.py index 8360304d..ac6b2931 100644 --- a/src/posit/connect/tasks.py +++ b/src/posit/connect/tasks.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import overload +from typing_extensions import overload from . import resources diff --git a/src/posit/connect/users.py b/src/posit/connect/users.py index 334617a5..a925df90 100644 --- a/src/posit/connect/users.py +++ b/src/posit/connect/users.py @@ -2,9 +2,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List, Literal - -from typing_extensions import NotRequired, Required, TypedDict, Unpack +from typing_extensions import ( + TYPE_CHECKING, + List, + Literal, + NotRequired, + Required, + TypedDict, + Unpack, +) from . import me from .content import Content diff --git a/src/posit/connect/vanities.py b/src/posit/connect/vanities.py index 5a483054..3ab9f4f2 100644 --- a/src/posit/connect/vanities.py +++ b/src/posit/connect/vanities.py @@ -1,6 +1,4 @@ -from typing import Callable, List, Optional - -from typing_extensions import NotRequired, Required, TypedDict, Unpack +from typing_extensions import Callable, List, NotRequired, Optional, Required, TypedDict, Unpack from .context import Context from .errors import ClientError diff --git a/src/posit/connect/variants.py b/src/posit/connect/variants.py index 9970c70e..2e518fb0 100644 --- a/src/posit/connect/variants.py +++ b/src/posit/connect/variants.py @@ -1,4 +1,4 @@ -from typing import List +from typing_extensions import List from .context import Context from .resources import BaseResource, Resources diff --git a/tests/posit/connect/external/test_databricks.py b/tests/posit/connect/external/test_databricks.py index 9861b907..134911b1 100644 --- a/tests/posit/connect/external/test_databricks.py +++ b/tests/posit/connect/external/test_databricks.py @@ -1,9 +1,9 @@ import base64 -from typing import Dict from unittest.mock import patch import pytest import responses +from typing_extensions import Dict from posit.connect import Client from posit.connect.external.databricks import ( diff --git a/tests/posit/connect/test_jobs.py b/tests/posit/connect/test_jobs.py index b2463d98..802dd850 100644 --- a/tests/posit/connect/test_jobs.py +++ b/tests/posit/connect/test_jobs.py @@ -1,8 +1,7 @@ -from typing import TYPE_CHECKING - import pytest import responses from requests.exceptions import HTTPError +from typing_extensions import TYPE_CHECKING from posit.connect.client import Client diff --git a/tests/posit/connect/test_resources.py b/tests/posit/connect/test_resources.py index a17d252b..52536b96 100644 --- a/tests/posit/connect/test_resources.py +++ b/tests/posit/connect/test_resources.py @@ -1,8 +1,9 @@ import warnings -from typing import Optional from unittest import mock from unittest.mock import Mock +from typing_extensions import Optional + from posit.connect.resources import BaseResource config = Mock() From 8f8b98266842f4132fa3fd92b837283fd77b5c7e Mon Sep 17 00:00:00 2001 From: tdstein Date: Tue, 17 Dec 2024 10:35:40 -0500 Subject: [PATCH 3/8] --wip-- [skip ci] --- src/posit/connect/content.py | 4 +++- src/posit/connect/jobs.py | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/posit/connect/content.py b/src/posit/connect/content.py index f7a3fdb8..9e793b7c 100644 --- a/src/posit/connect/content.py +++ b/src/posit/connect/content.py @@ -19,6 +19,8 @@ overload, ) +from posit.connect.jobs import _Jobs, Job + from . import tasks from ._api import ApiDictEndpoint, JsonifiableDict from .bundles import Bundles @@ -514,7 +516,7 @@ def tags(self) -> ContentItemTags: @property def jobs(self) -> Jobs: path = posixpath.join(self._path, "jobs") - return _ResourceSequence(self._ctx, path, uid="key") + return _Jobs(self._ctx, path, uid="key") @property @requires(version="2024.11.0") diff --git a/src/posit/connect/jobs.py b/src/posit/connect/jobs.py index 959c1dab..1b244fcb 100644 --- a/src/posit/connect/jobs.py +++ b/src/posit/connect/jobs.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any, List + from typing_extensions import ( Iterable, Literal, @@ -7,7 +9,7 @@ runtime_checkable, ) -from .resources import Resource, ResourceSequence +from .resources import Resource, ResourceSequence, _Resource, _ResourceSequence JobTag = Literal[ "unknown", @@ -39,6 +41,7 @@ StatusCode = Literal[0, 1, 2] +@runtime_checkable class Job(Resource, Protocol): def destroy(self) -> None: """Destroy the job. @@ -53,6 +56,11 @@ def destroy(self) -> None: """ +class _Job(_Resource): + def wait_for(self) -> None: + pass + + @runtime_checkable class Jobs(ResourceSequence[Job], Protocol): def fetch(self) -> Iterable[Job]: @@ -165,3 +173,9 @@ def find_by( This action requires administrator, owner, or collaborator privileges. """ ... + + +class _Jobs(_ResourceSequence[Job]): + def fetch(self, **conditions) -> Iterable[Any]: + resources = super().fetch(**conditions) + return [_Job(**resource) for resource in resources] From f4c5445336419f04a1d614a45d676771162c0a60 Mon Sep 17 00:00:00 2001 From: tdstein Date: Tue, 17 Dec 2024 11:30:33 -0500 Subject: [PATCH 4/8] feat: add factory support to resource sequences --- src/posit/connect/content.py | 4 +--- src/posit/connect/jobs.py | 15 +------------ src/posit/connect/resources.py | 40 +++++++++++++++++++++++----------- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/posit/connect/content.py b/src/posit/connect/content.py index 9e793b7c..f7a3fdb8 100644 --- a/src/posit/connect/content.py +++ b/src/posit/connect/content.py @@ -19,8 +19,6 @@ overload, ) -from posit.connect.jobs import _Jobs, Job - from . import tasks from ._api import ApiDictEndpoint, JsonifiableDict from .bundles import Bundles @@ -516,7 +514,7 @@ def tags(self) -> ContentItemTags: @property def jobs(self) -> Jobs: path = posixpath.join(self._path, "jobs") - return _Jobs(self._ctx, path, uid="key") + return _ResourceSequence(self._ctx, path, uid="key") @property @requires(version="2024.11.0") diff --git a/src/posit/connect/jobs.py b/src/posit/connect/jobs.py index 1b244fcb..2fdf8b4e 100644 --- a/src/posit/connect/jobs.py +++ b/src/posit/connect/jobs.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Any, List - from typing_extensions import ( Iterable, Literal, @@ -9,7 +7,7 @@ runtime_checkable, ) -from .resources import Resource, ResourceSequence, _Resource, _ResourceSequence +from .resources import Resource, ResourceSequence JobTag = Literal[ "unknown", @@ -56,11 +54,6 @@ def destroy(self) -> None: """ -class _Job(_Resource): - def wait_for(self) -> None: - pass - - @runtime_checkable class Jobs(ResourceSequence[Job], Protocol): def fetch(self) -> Iterable[Job]: @@ -173,9 +166,3 @@ def find_by( This action requires administrator, owner, or collaborator privileges. """ ... - - -class _Jobs(_ResourceSequence[Job]): - def fetch(self, **conditions) -> Iterable[Any]: - resources = super().fetch(**conditions) - return [_Job(**resource) for resource in resources] diff --git a/src/posit/connect/resources.py b/src/posit/connect/resources.py index 7095c8fa..42a59a77 100644 --- a/src/posit/connect/resources.py +++ b/src/posit/connect/resources.py @@ -3,6 +3,7 @@ import posixpath import warnings from abc import ABC +from typing import ItemsView, cast from typing_extensions import ( TYPE_CHECKING, @@ -73,6 +74,8 @@ def __init__(self, ctx: Context, path: str, /, **attributes): class Resource(Protocol): def __getitem__(self, key: Hashable) -> Any: ... + def items(self) -> ItemsView: ... + class _Resource(dict, Resource): def __init__(self, ctx: Context, path: str, **attributes): @@ -92,6 +95,10 @@ def update(self, **attributes): # type: ignore[reportIncompatibleMethodOverride T = TypeVar("T", bound=Resource) +class ResourceFactory(Protocol): + def __call__(self, ctx: Context, path: str, **attributes) -> Resource: ... + + class ResourceSequence(Protocol[T]): @overload def __getitem__(self, index: SupportsIndex, /) -> T: ... @@ -109,10 +116,17 @@ def __repr__(self) -> str: ... class _ResourceSequence(Sequence[T], ResourceSequence[T]): - def __init__(self, ctx: Context, path: str, *, uid: str = "guid"): + def __init__( + self, + ctx: Context, + path: str, + factory: ResourceFactory = _Resource, + uid: str = "guid", + ): self._ctx = ctx self._path = path self._uid = uid + self._factory = factory def __getitem__(self, index): return list(self.fetch())[index] @@ -129,32 +143,32 @@ def __str__(self) -> str: def __repr__(self) -> str: return repr(self.fetch()) - def create(self, **attributes: Any) -> Any: + def create(self, **attributes: Any) -> T: response = self._ctx.client.post(self._path, json=attributes) result = response.json() uid = result[self._uid] path = posixpath.join(self._path, uid) - return _Resource(self._ctx, path, **result) + return cast(T, self._factory(self._ctx, path, **result)) - def fetch(self, **conditions) -> Iterable[Any]: + def fetch(self, **conditions) -> Iterable[T]: response = self._ctx.client.get(self._path, params=conditions) results = response.json() - resources = [] + resources: List[T] = [] for result in results: uid = result[self._uid] path = posixpath.join(self._path, uid) - resource = _Resource(self._ctx, path, **result) + resource = cast(T, self._factory(self._ctx, path, **result)) resources.append(resource) return resources - def find(self, *args: str) -> Any: + def find(self, *args: str) -> T: path = posixpath.join(self._path, *args) response = self._ctx.client.get(path) result = response.json() - return _Resource(self._ctx, path, **result) + return cast(T, self._factory(self._ctx, path, **result)) - def find_by(self, **conditions) -> Any | None: + def find_by(self, **conditions) -> T | None: """ Find the first record matching the specified conditions. @@ -169,12 +183,12 @@ def find_by(self, **conditions) -> Any | None: Optional[T] The first record matching the conditions, or `None` if no match is found. """ - collection = self.fetch(**conditions) + collection: Iterable[T] = self.fetch(**conditions) return next((v for v in collection if v.items() >= conditions.items()), None) -class _PaginatedResourceSequence(_ResourceSequence): - def fetch(self, **conditions): +class _PaginatedResourceSequence(_ResourceSequence[T]): + def fetch(self, **conditions) -> Iterator[T]: paginator = Paginator(self._ctx, self._path, dict(**conditions)) for page in paginator.fetch_pages(): resources = [] @@ -182,6 +196,6 @@ def fetch(self, **conditions): for result in results: uid = result[self._uid] path = posixpath.join(self._path, uid) - resource = _Resource(self._ctx, path, **result) + resource = cast(T, self._factory(self._ctx, path, **result)) resources.append(resource) yield from resources From 39e1eb0f02e90778eefd552396f8bac1157e44c4 Mon Sep 17 00:00:00 2001 From: tdstein Date: Tue, 17 Dec 2024 11:52:34 -0500 Subject: [PATCH 5/8] feat: add factory support to resource sequences --- src/posit/connect/client.py | 7 +++-- src/posit/connect/resources.py | 52 ++++++++++++++++++---------------- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/posit/connect/client.py b/src/posit/connect/client.py index 6ef22c21..10b02c45 100644 --- a/src/posit/connect/client.py +++ b/src/posit/connect/client.py @@ -5,6 +5,9 @@ from requests import Response, Session from typing_extensions import TYPE_CHECKING, overload +from posit.connect.environments import Environment +from posit.connect.packages import Package + from . import hooks, me from .auth import Auth from .config import Config @@ -298,7 +301,7 @@ def oauth(self) -> OAuth: @property @requires(version="2024.11.0") def packages(self) -> Packages: - return _PaginatedResourceSequence(self._ctx, "v1/packages", uid="name") + return _PaginatedResourceSequence[Package](self._ctx, "v1/packages", uid="name") @property def vanities(self) -> Vanities: @@ -311,7 +314,7 @@ def system(self) -> System: @property @requires(version="2023.05.0") def environments(self) -> Environments: - return _ResourceSequence(self._ctx, "v1/environments") + return _ResourceSequence[Environment](self._ctx, "v1/environments") def __del__(self): """Close the session when the Client instance is deleted.""" diff --git a/src/posit/connect/resources.py b/src/posit/connect/resources.py index 42a59a77..50811bf4 100644 --- a/src/posit/connect/resources.py +++ b/src/posit/connect/resources.py @@ -3,7 +3,7 @@ import posixpath import warnings from abc import ABC -from typing import ItemsView, cast +from typing import ItemsView, Type, cast from typing_extensions import ( TYPE_CHECKING, @@ -92,41 +92,42 @@ def update(self, **attributes): # type: ignore[reportIncompatibleMethodOverride super().update(**result) -T = TypeVar("T", bound=Resource) +_T = TypeVar("_T", bound=Resource) +_T_co = TypeVar("_T_co", bound=Resource, covariant=True) -class ResourceFactory(Protocol): - def __call__(self, ctx: Context, path: str, **attributes) -> Resource: ... +class ResourceFactory(Protocol[_T_co]): + def __call__(self, ctx: Context, path: str, **attributes: Any) -> _T_co: ... -class ResourceSequence(Protocol[T]): +class ResourceSequence(Protocol[_T]): @overload - def __getitem__(self, index: SupportsIndex, /) -> T: ... + def __getitem__(self, index: SupportsIndex, /) -> _T: ... @overload - def __getitem__(self, index: slice, /) -> List[T]: ... + def __getitem__(self, index: slice, /) -> List[_T]: ... def __len__(self) -> int: ... - def __iter__(self) -> Iterator[T]: ... + def __iter__(self) -> Iterator[_T]: ... def __str__(self) -> str: ... def __repr__(self) -> str: ... -class _ResourceSequence(Sequence[T], ResourceSequence[T]): +class _ResourceSequence(Sequence[_T], ResourceSequence[_T]): def __init__( self, ctx: Context, path: str, - factory: ResourceFactory = _Resource, + factory: ResourceFactory[_T] | None = None, uid: str = "guid", ): self._ctx = ctx self._path = path self._uid = uid - self._factory = factory + self._factory = factory or cast(ResourceFactory[_T], _Resource) def __getitem__(self, index): return list(self.fetch())[index] @@ -134,7 +135,7 @@ def __getitem__(self, index): def __len__(self) -> int: return len(list(self.fetch())) - def __iter__(self) -> Iterator[T]: + def __iter__(self) -> Iterator[_T]: return iter(self.fetch()) def __str__(self) -> str: @@ -143,32 +144,34 @@ def __str__(self) -> str: def __repr__(self) -> str: return repr(self.fetch()) - def create(self, **attributes: Any) -> T: + def create(self, **attributes: Any) -> _T: response = self._ctx.client.post(self._path, json=attributes) result = response.json() uid = result[self._uid] path = posixpath.join(self._path, uid) - return cast(T, self._factory(self._ctx, path, **result)) + resource = self._factory(self._ctx, path, **result) + return resource - def fetch(self, **conditions) -> Iterable[T]: + def fetch(self, **conditions: Any) -> Iterable[_T]: response = self._ctx.client.get(self._path, params=conditions) results = response.json() - resources: List[T] = [] + resources: List[_T] = [] for result in results: uid = result[self._uid] path = posixpath.join(self._path, uid) - resource = cast(T, self._factory(self._ctx, path, **result)) + resource = self._factory(self._ctx, path, **result) resources.append(resource) return resources - def find(self, *args: str) -> T: + def find(self, *args: str) -> _T: path = posixpath.join(self._path, *args) response = self._ctx.client.get(path) result = response.json() - return cast(T, self._factory(self._ctx, path, **result)) + resource = self._factory(self._ctx, path, **result) + return resource - def find_by(self, **conditions) -> T | None: + def find_by(self, **conditions: Any) -> _T | None: """ Find the first record matching the specified conditions. @@ -183,12 +186,12 @@ def find_by(self, **conditions) -> T | None: Optional[T] The first record matching the conditions, or `None` if no match is found. """ - collection: Iterable[T] = self.fetch(**conditions) + collection = self.fetch(**conditions) return next((v for v in collection if v.items() >= conditions.items()), None) -class _PaginatedResourceSequence(_ResourceSequence[T]): - def fetch(self, **conditions) -> Iterator[T]: +class _PaginatedResourceSequence(_ResourceSequence[_T]): + def fetch(self, **conditions: Any) -> Iterable[_T]: paginator = Paginator(self._ctx, self._path, dict(**conditions)) for page in paginator.fetch_pages(): resources = [] @@ -196,6 +199,7 @@ def fetch(self, **conditions) -> Iterator[T]: for result in results: uid = result[self._uid] path = posixpath.join(self._path, uid) - resource = cast(T, self._factory(self._ctx, path, **result)) + resource = self._factory(self._ctx, path, **result) + resources.append(resource) yield from resources From e0d20eea14de4af87c665f16eeb65de111fb2b19 Mon Sep 17 00:00:00 2001 From: tdstein Date: Tue, 17 Dec 2024 13:49:29 -0500 Subject: [PATCH 6/8] feat: add factory support to resource sequences --- src/posit/connect/resources.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/posit/connect/resources.py b/src/posit/connect/resources.py index 50811bf4..e820b4f0 100644 --- a/src/posit/connect/resources.py +++ b/src/posit/connect/resources.py @@ -3,7 +3,7 @@ import posixpath import warnings from abc import ABC -from typing import ItemsView, Type, cast +from typing import ItemsView from typing_extensions import ( TYPE_CHECKING, @@ -92,8 +92,8 @@ def update(self, **attributes): # type: ignore[reportIncompatibleMethodOverride super().update(**result) -_T = TypeVar("_T", bound=Resource) -_T_co = TypeVar("_T_co", bound=Resource, covariant=True) +_T = TypeVar("_T", bound=_Resource) +_T_co = TypeVar("_T_co", bound=_Resource, covariant=True) class ResourceFactory(Protocol[_T_co]): @@ -121,13 +121,13 @@ def __init__( self, ctx: Context, path: str, - factory: ResourceFactory[_T] | None = None, + factory: ResourceFactory[_T] = _Resource, uid: str = "guid", ): self._ctx = ctx self._path = path + self._factory = factory self._uid = uid - self._factory = factory or cast(ResourceFactory[_T], _Resource) def __getitem__(self, index): return list(self.fetch())[index] From 7977e85a6270974db86087e086333eddc50ee3b7 Mon Sep 17 00:00:00 2001 From: tdstein Date: Tue, 17 Dec 2024 13:53:26 -0500 Subject: [PATCH 7/8] feat: add factory support to resource sequences --- src/posit/connect/resources.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/posit/connect/resources.py b/src/posit/connect/resources.py index e820b4f0..faf8c14d 100644 --- a/src/posit/connect/resources.py +++ b/src/posit/connect/resources.py @@ -3,7 +3,7 @@ import posixpath import warnings from abc import ABC -from typing import ItemsView +from typing import ItemsView, cast from typing_extensions import ( TYPE_CHECKING, @@ -92,8 +92,8 @@ def update(self, **attributes): # type: ignore[reportIncompatibleMethodOverride super().update(**result) -_T = TypeVar("_T", bound=_Resource) -_T_co = TypeVar("_T_co", bound=_Resource, covariant=True) +_T = TypeVar("_T", bound=Resource) +_T_co = TypeVar("_T_co", bound=Resource, covariant=True) class ResourceFactory(Protocol[_T_co]): @@ -121,13 +121,13 @@ def __init__( self, ctx: Context, path: str, - factory: ResourceFactory[_T] = _Resource, + factory: ResourceFactory[_T_co] = _Resource, uid: str = "guid", ): self._ctx = ctx self._path = path - self._factory = factory self._uid = uid + self._factory = factory def __getitem__(self, index): return list(self.fetch())[index] @@ -150,6 +150,7 @@ def create(self, **attributes: Any) -> _T: uid = result[self._uid] path = posixpath.join(self._path, uid) resource = self._factory(self._ctx, path, **result) + resource = cast(_T, resource) return resource def fetch(self, **conditions: Any) -> Iterable[_T]: @@ -160,6 +161,7 @@ def fetch(self, **conditions: Any) -> Iterable[_T]: uid = result[self._uid] path = posixpath.join(self._path, uid) resource = self._factory(self._ctx, path, **result) + resource = cast(_T, resource) resources.append(resource) return resources @@ -169,6 +171,7 @@ def find(self, *args: str) -> _T: response = self._ctx.client.get(path) result = response.json() resource = self._factory(self._ctx, path, **result) + resource = cast(_T, resource) return resource def find_by(self, **conditions: Any) -> _T | None: @@ -200,6 +203,6 @@ def fetch(self, **conditions: Any) -> Iterable[_T]: uid = result[self._uid] path = posixpath.join(self._path, uid) resource = self._factory(self._ctx, path, **result) - + resource = cast(_T, resource) resources.append(resource) yield from resources From 35e03644e408e56eb26708c2f0232b0bab7fbb2f Mon Sep 17 00:00:00 2001 From: tdstein Date: Tue, 17 Dec 2024 13:56:32 -0500 Subject: [PATCH 8/8] fix imports --- src/posit/connect/resources.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/posit/connect/resources.py b/src/posit/connect/resources.py index faf8c14d..5bd5bd5a 100644 --- a/src/posit/connect/resources.py +++ b/src/posit/connect/resources.py @@ -3,12 +3,12 @@ import posixpath import warnings from abc import ABC -from typing import ItemsView, cast from typing_extensions import ( TYPE_CHECKING, Any, Hashable, + ItemsView, Iterable, Iterator, List, @@ -16,6 +16,7 @@ Sequence, SupportsIndex, TypeVar, + cast, overload, )