Skip to content

Commit

Permalink
feat: adds visits find and find_one (#163)
Browse files Browse the repository at this point in the history
  • Loading branch information
tdstein authored Apr 11, 2024
1 parent db1671b commit 5aacc70
Show file tree
Hide file tree
Showing 6 changed files with 499 additions and 0 deletions.
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
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.
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

0 comments on commit 5aacc70

Please sign in to comment.