-
Notifications
You must be signed in to change notification settings - Fork 8
refactor: inject url path parts instead of endpoints #315
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 32 commits
0533f19
6b79912
279fcd6
e349870
533839b
1066ca3
437c515
a1ca377
82b9b7e
6b8126d
b64f3e7
f57340d
72b62ac
f1d6f42
dd74d60
fb52c83
bbbd6b4
bc2cfcb
9c3d6dd
9019386
4bfe3f8
a721b61
107ee85
a070f0a
d196271
d87cfe7
2add280
97d24f6
7eeb054
03accf8
77f8a38
ac488c7
b3ff1cd
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,3 +1,4 @@ | ||
| import posixpath | ||
| import warnings | ||
| from abc import ABC, abstractmethod | ||
| from dataclasses import dataclass | ||
|
|
@@ -50,84 +51,118 @@ def __init__(self, params: ResourceParameters) -> None: | |
|
|
||
|
|
||
| class Active(ABC, Resource): | ||
| def __init__(self, ctx: Context, parent: Optional["Active"] = None, **kwargs): | ||
| """A base class representing an active resource. | ||
| def __init__(self, ctx: Context, path: str, /, **attributes): | ||
| """A dict abstraction for any HTTP endpoint that returns a singular resource. | ||
|
|
||
| Extends the `Resource` class and provides additional functionality for via the session context and an optional parent resource. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| ctx : Context | ||
| The context object containing the session and URL for API interactions. | ||
| parent : Optional[Active], optional | ||
| An optional parent resource that establishes a hierarchical relationship, by default None. | ||
| **kwargs : dict | ||
| Additional keyword arguments passed to the parent `Resource` class. | ||
| path : str | ||
| The HTTP path component for the resource endpoint | ||
| **attributes : dict | ||
| Resource attributes passed | ||
| """ | ||
| params = ResourceParameters(ctx.session, ctx.url) | ||
| super().__init__(params, **kwargs) | ||
| super().__init__(params, **attributes) | ||
| self._ctx = ctx | ||
| self._parent = parent | ||
| self._path = path | ||
|
|
||
|
|
||
| T = TypeVar("T", bound="Active") | ||
| """A type variable that is bound to the `Active` class""" | ||
|
|
||
|
|
||
| class ActiveSequence(ABC, Generic[T], Sequence[T]): | ||
| def __init__(self, ctx: Context, parent: Optional[Active] = None): | ||
| """A sequence abstraction for any HTTP GET endpoint that returns a collection. | ||
| """A sequence for any HTTP GET endpoint that returns a collection.""" | ||
|
|
||
| _cache: Optional[List[T]] | ||
|
|
||
| It lazily fetches data on demand, caches the results, and allows for standard sequence operations like indexing and slicing. | ||
| def __init__(self, ctx: Context, path: str, uid: str = "guid"): | ||
| """A sequence abstraction for any HTTP GET endpoint that returns a collection. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| ctx : Context | ||
| The context object that holds the HTTP session used for sending the GET request. | ||
| parent : Optional[Active], optional | ||
| An optional parent resource to establish a nested relationship, by default None. | ||
| The context object containing the session and URL for API interactions. | ||
| path : str | ||
| The HTTP path component for the collection endpoint | ||
| uid : str, optional | ||
| The field name of that uniquely identifiers an instance of T, by default "guid" | ||
| """ | ||
| super().__init__() | ||
| self._ctx = ctx | ||
| self._parent = parent | ||
| self._cache: Optional[List[T]] = None | ||
| self._path = path | ||
| self._uid = uid | ||
| self._cache = None | ||
|
|
||
| @property | ||
| @abstractmethod | ||
| def _endpoint(self) -> str: | ||
| def _create_instance(self, path: str, /, **kwargs: Any) -> T: | ||
| """Create an instance of 'T'.""" | ||
| raise NotImplementedError() | ||
|
|
||
| def cached(self) -> bool: | ||
tdstein marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| """Returns True if the collection is cached. | ||
|
|
||
| Returns | ||
| ------- | ||
| bool | ||
|
|
||
| See Also | ||
| -------- | ||
| reload | ||
| """ | ||
| Abstract property to define the endpoint URL for the GET request. | ||
| return self._cache is not None | ||
|
|
||
| Subclasses must implement this property to return the API endpoint URL that will | ||
| be queried to fetch the data. | ||
| def reload(self) -> Self: | ||
| """Reloads the collection from Connect. | ||
tdstein marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| Returns | ||
| ------- | ||
| str | ||
| The API endpoint URL. | ||
| Self | ||
| """ | ||
| raise NotImplementedError() | ||
| self._cache = None | ||
| return self | ||
|
|
||
| def _fetch(self) -> List[T]: | ||
| """Fetch the collection. | ||
|
|
||
| Fetches the collection directly from Connect. This operation does not effect the cache state. | ||
|
|
||
| Returns | ||
| ------- | ||
| List[T] | ||
| """ | ||
| endpoint = self._ctx.url + self._path | ||
| response = self._ctx.session.get(endpoint) | ||
| results = response.json() | ||
| return [self._to_instance(result) for result in results] | ||
|
|
||
| def _to_instance(self, result: dict) -> T: | ||
| """Converts a result into an instance of T.""" | ||
| uid = result[self._uid] | ||
| path = posixpath.join(self._path, uid) | ||
| return self._create_instance(path, **result) | ||
|
|
||
| @property | ||
| def _data(self) -> List[T]: | ||
| """ | ||
| Fetch and cache the data from the API. | ||
| """Get the collection. | ||
|
|
||
| This method sends a GET request to the `_endpoint` and parses the response as a list of JSON objects. | ||
| Each JSON object is used to instantiate an item of type `T` using the class specified by `_cls`. | ||
| The results are cached after the first request and reused for subsequent access unless reloaded. | ||
| Fetches the collection from Connect and caches the result. Subsequent invocations return the cached results unless the cache is explicitly reset. | ||
|
|
||
| Returns | ||
| ------- | ||
| List[T] | ||
| A list of items of type `T` representing the fetched data. | ||
| """ | ||
| if self._cache: | ||
| return self._cache | ||
|
|
||
| response = self._ctx.session.get(self._endpoint) | ||
| results = response.json() | ||
| self._cache = [self._create_instance(**result) for result in results] | ||
| See Also | ||
| -------- | ||
| cached | ||
| reload | ||
| """ | ||
| if self._cache is None: | ||
tdstein marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| self._cache = self._fetch() | ||
| return self._cache | ||
|
|
||
| @overload | ||
|
|
@@ -148,52 +183,18 @@ def __str__(self) -> str: | |
| def __repr__(self) -> str: | ||
| return repr(self._data) | ||
|
|
||
| @abstractmethod | ||
| def _create_instance(self, **kwargs) -> T: | ||
| """Create an instance of 'T'. | ||
|
|
||
| Returns | ||
| ------- | ||
| T | ||
| """ | ||
| raise NotImplementedError() | ||
| class ActiveFinderMethods(ActiveSequence[T], ABC): | ||
| """Finder methods. | ||
|
|
||
| def reload(self) -> Self: | ||
| """ | ||
| Clear the cache and reload the data from the API on the next access. | ||
|
|
||
| Returns | ||
| ------- | ||
| ActiveSequence | ||
| The current instance with cleared cache, ready to reload data on next access. | ||
| """ | ||
| self._cache = None | ||
| return self | ||
|
|
||
|
|
||
| class ActiveFinderMethods(ActiveSequence[T], ABC, Generic[T]): | ||
| def __init__(self, ctx: Context, parent: Optional[Active] = None, uid: str = "guid"): | ||
| """Finder methods. | ||
|
|
||
| Provides various finder methods for locating records in any endpoint supporting HTTP GET requests. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| ctx : Context | ||
| The context containing the HTTP session used to interact with the API. | ||
| parent : Optional[Active], optional | ||
| Optional parent resource for maintaining hierarchical relationships, by default None | ||
| uid : str, optional | ||
| The default field name used to uniquely identify records, by default "guid" | ||
| """ | ||
| super().__init__(ctx, parent) | ||
| self._uid = uid | ||
| Provides various finder methods for locating records in any endpoint supporting HTTP GET requests. | ||
| """ | ||
|
|
||
| def find(self, uid) -> T: | ||
| """ | ||
| Find a record by its unique identifier. | ||
|
|
||
| Fetches a record either by searching the cache or by making a GET request to the endpoint. | ||
| If the cache is already populated, it is checked first for matching record. If not, a conventional GET request is made to the Connect server. | ||
|
|
||
| Parameters | ||
| ---------- | ||
|
|
@@ -203,26 +204,17 @@ def find(self, uid) -> T: | |
| Returns | ||
| ------- | ||
| T | ||
|
|
||
| Raises | ||
| ------ | ||
| ValueError | ||
| If no record is found. | ||
| """ | ||
| # todo - add some more comments about this | ||
| if self._cache: | ||
| if self.cached(): | ||
| conditions = {self._uid: uid} | ||
| result = self.find_by(**conditions) | ||
| else: | ||
| endpoint = self._endpoint + uid | ||
| response = self._ctx.session.get(endpoint) | ||
| result = response.json() | ||
| result = self._create_instance(**result) | ||
|
|
||
| if not result: | ||
| raise ValueError(f"Failed to find instance where {self._uid} is '{uid}'") | ||
| if result: | ||
| return result | ||
|
|
||
| return result | ||
| endpoint = self._ctx.url + self._path + uid | ||
| response = self._ctx.session.get(endpoint) | ||
| result = response.json() | ||
| return self._to_instance(result) | ||
|
Comment on lines
+195
to
+198
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. Note that I removed the call to invalidate the cache that existed a few commits before. After some additional consideration, I concluded that invalidating the cache is an unwanted side effect. I think there is still an argument for invalidating the cache or appending the instance to the cached list. But, I don't think we have a good enough understanding of the side effects to proceed with either implementation.
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. Why not just call If the cache exists, Then both methods have the same quirks. (Where as
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. I think in either situation, we end up with conflicting ideas. If we always depend on Today, I think the obvious solution would be to always call the HTTP GET method to get the value from the server. This will sometimes be slightly slower than an in-memory list scan. But in reality, it's going to be a negligible difference. The weird edge case with this solution is when another process creates the value fetched by GET after the tl;dr - the speed up via
|
||
|
|
||
| def find_by(self, **conditions: Any) -> Optional[T]: | ||
| """ | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.