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

feat: adds visits find and find_one #163

Merged
merged 5 commits into from
Apr 11, 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
5 changes: 5 additions & 0 deletions src/posit/connect/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .oauth import OAuthIntegration
from .content import Content
from .users import User, Users
from .visits import Visits


class Client:
Expand Down Expand Up @@ -95,6 +96,10 @@ def content(self) -> Content:
"""
return Content(config=self.config, session=self.session)

@property
def visits(self) -> Visits:
return Visits(self.config, self.session)

def __del__(self):
"""Close the session when the Client instance is deleted."""
if hasattr(self, "session") and self.session is not None:
Expand Down
74 changes: 74 additions & 0 deletions src/posit/connect/cursors.py
tdstein marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from __future__ import annotations

from dataclasses import dataclass, make_dataclass
from typing import Generator, List

import requests

# The maximum page size supported by the API.
_MAX_PAGE_SIZE = 500


@dataclass
class CursorPage:
paging: dict
results: List[dict]


class CursorPaginator:
def __init__(self, session: requests.Session, url: str, params: dict = {}) -> None:
self.session = session
self.url = url
self.params = params

def fetch_results(self) -> List[dict]:
"""Fetch results.

Collects all results from all pages.

Returns
-------
List[dict]
A coalesced list of all results.
"""
results = []
for page in self.fetch_pages():
results.extend(page.results)
return results

def fetch_pages(self) -> Generator[CursorPage, None, None]:
"""Fetch pages.

Yields
------
Generator[Page, None, None]
"""
next = None
while True:
page = self.fetch_page(next)
yield page
cursors: dict = page.paging.get("cursors", {})
next = cursors.get("next")
if not next:
# stop if a next page is not defined
return

def fetch_page(self, next: str | None = None) -> CursorPage:
"""Fetch a page.

Parameters
----------
next : str | None, optional
the next page identifier or None to fetch the first page, by default None

Returns
-------
Page
"""
params = {
**self.params,
"next": next,
"limit": _MAX_PAGE_SIZE,
}
response = self.session.get(self.url, params=params)
return CursorPage(**response.json())
240 changes: 240 additions & 0 deletions src/posit/connect/visits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
from __future__ import annotations

from typing import List, overload

from . import urls

from .cursors import CursorPaginator
from .resources import Resource, Resources


class Visit(Resource):
@property
def content_guid(self) -> str:
"""The associated unique content identifier.

Returns
-------
str
"""
return self["content_guid"]

@property
def user_guid(self) -> str:
"""The associated unique user identifier.

Returns
-------
str
"""
return self["user_guid"]

@property
def rendering_id(self) -> int | None:
"""The render id associated with the visit.

Returns
-------
int | None
The render id, or None if the associated content type is static.
"""
return self["rendering_id"]

@property
def bundle_id(self) -> int:
"""The bundle id associated with the visit.

Returns
-------
int
"""
return self["bundle_id"]

@property
def variant_key(self) -> str | None:
"""The variant key associated with the visit.

Returns
-------
str | None
The variant key, or None if the associated content type is static.
"""
return self.get("variant_key")

@property
def time(self) -> str:
"""The visit timestamp.

Returns
-------
str
"""
return self["time"]

@property
def data_version(self) -> int:
"""The data version.

Returns
-------
int
"""
return self["data_version"]

@property
def path(self) -> str:
"""The path requested by the user.

Returns
-------
str
"""
return self["path"]


class Visits(Resources):
@overload
def find(
self,
content_guid: str = ...,
min_data_version: int = ...,
start: str = ...,
end: str = ...,
) -> List[Visit]:
"""Find visits.

Parameters
----------
content_guid : str, optional
Filter by an associated unique content identifer, by default ...
min_data_version : int, optional
Filter by a minimum data version, by default ...
start : str, optional
Filter by the start time, by default ...
end : str, optional
Filter by the end time, by default ...

Returns
-------
List[Visit]
"""
...

@overload
def find(self, *args, **kwargs) -> List[Visit]:
"""Find visits.

Returns
-------
List[Visit]
"""
...

def find(self, *args, **kwargs) -> List[Visit]:
"""Find visits.

Returns
-------
List[Visit]
"""
params = dict(*args, **kwargs)
params = rename_params(params)

path = "/v1/instrumentation/content/visits"
url = urls.append_path(self.config.url, path)
paginator = CursorPaginator(self.session, url, params=params)
results = paginator.fetch_results()
return [
Visit(
config=self.config,
session=self.session,
**result,
)
for result in results
]

@overload
def find_one(
self,
content_guid: str = ...,
min_data_version: int = ...,
start: str = ...,
end: str = ...,
) -> Visit | None:
"""Find a visit.

Parameters
----------
content_guid : str, optional
Filter by an associated unique content identifer, by default ...
min_data_version : int, optional
Filter by a minimum data version, by default ...
start : str, optional
Filter by the start time, by default ...
end : str, optional
Filter by the end time, by default ...

Returns
-------
Visit | None
_description_
"""
...

@overload
def find_one(self, *args, **kwargs) -> Visit | None:
"""Find a visit.

Returns
-------
Visit | None
"""
...

def find_one(self, *args, **kwargs) -> Visit | None:
"""Find a visit.

Returns
-------
Visit | None
"""
params = dict(*args, **kwargs)
params = rename_params(params)
path = "/v1/instrumentation/content/visits"
url = urls.append_path(self.config.url, path)
paginator = CursorPaginator(self.session, url, params=params)
pages = paginator.fetch_pages()
results = (result for page in pages for result in page.results)
visits = (
Visit(
config=self.config,
session=self.session,
**result,
)
for result in results
)
return next(visits, None)


def rename_params(params: dict) -> dict:
"""Rename params from the internal to the external signature.

The API accepts `from` as a querystring parameter. Since `from` is a reserved word in Python, the SDK uses the name `start` instead. The querystring parameter `to` takes the same form for consistency.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note this change! Open to other ideas if there are strong opinions.


Parameters
----------
params : dict

Returns
-------
dict
"""
if "start" in params:
params["from"] = params["start"]
del params["start"]

if "end" in params:
params["to"] = params["end"]
del params["end"]

return params
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"paging": {
"cursors": {
"previous": "23948901087"
},
"first": "http://localhost:3443/__api__/v1/instrumentation/content/visits",
"previous": "http://localhost:3443/__api__/v1/instrumentation/content/visits?previous=23948901087"
},
"results": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"paging": {
"cursors": {
"previous": "23948901087",
"next": "23948901087"
},
"first": "http://localhost:3443/__api__/v1/instrumentation/content/visits",
"previous": "http://localhost:3443/__api__/v1/instrumentation/content/visits?previous=23948901087",
"next": "http://localhost:3443/__api__/v1/instrumentation/content/visits?next=23948901087",
"last": "http://localhost:3443/__api__/v1/instrumentation/content/visits?last=true"
},
"results": [
{
"content_guid": "bd1d2285-6c80-49af-8a83-a200effe3cb3",
"user_guid": "08e3a41d-1f8e-47f2-8855-f05ea3b0d4b2",
"variant_key": "HidI2Kwq",
"rendering_id": 7,
"bundle_id": 33,
"time": "2018-09-15T18:00:00-05:00",
"data_version": 1,
"path": "/logs"
}
]
}
Loading
Loading