Skip to content

Commit

Permalink
feat: adds usage (#166)
Browse files Browse the repository at this point in the history
  • Loading branch information
tdstein authored Apr 12, 2024
1 parent f68f0c4 commit 073c01d
Show file tree
Hide file tree
Showing 5 changed files with 378 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 @@ -11,6 +11,7 @@
from .config import Config
from .oauth import OAuthIntegration
from .content import Content
from .usage import Usage
from .users import User, Users
from .visits import Visits

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

@property
def usage(self) -> Usage:
return Usage(self.config, self.session)

@property
def visits(self) -> Visits:
return Visits(self.config, self.session)
Expand Down
207 changes: 207 additions & 0 deletions src/posit/connect/usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
from __future__ import annotations

from typing import List, overload

from . import urls

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


class UsageEvent(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 started(self) -> str:
"""The started timestamp.
Returns
-------
str
"""
return self["started"]

@property
def ended(self) -> str:
"""The ended timestamp.
Returns
-------
str
"""
return self["ended"]

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


class Usage(Resources):
@overload
def find(
self,
content_guid: str = ...,
min_data_version: int = ...,
start: str = ...,
end: str = ...,
) -> List[UsageEvent]:
"""Find usage.
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[UsageEvent]
"""
...

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

def find(self, *args, **kwargs) -> List[UsageEvent]:
"""Find usage.
Returns
-------
List[UsageEvent]
"""
params = dict(*args, **kwargs)
params = rename_params(params)

path = "/v1/instrumentation/shiny/usage"
url = urls.append(self.config.url, path)
paginator = CursorPaginator(self.session, url, params=params)
results = paginator.fetch_results()
return [
UsageEvent(
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 = ...,
) -> UsageEvent | None:
"""Find a usage event.
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
-------
UsageEvent | None
"""
...

@overload
def find_one(self, *args, **kwargs) -> UsageEvent | None:
"""Find a usage event.
Returns
-------
UsageEvent | None
"""
...

def find_one(self, *args, **kwargs) -> UsageEvent | None:
"""Find a usage event.
Returns
-------
UsageEvent | None
"""
params = dict(*args, **kwargs)
params = rename_params(params)
path = "/v1/instrumentation/shiny/usage"
url = urls.append(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 = (
UsageEvent(
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,21 @@
{
"paging": {
"cursors": {
"previous": "23948901087",
"next": "23948901087"
},
"first": "http://localhost:3443/__api__/v1/instrumentation/shiny/usage",
"previous": "http://localhost:3443/__api__/v1/instrumentation/shiny/usage?previous=23948901087",
"next": "http://localhost:3443/__api__/v1/instrumentation/shiny/usage?next=23948901087",
"last": "http://localhost:3443/__api__/v1/instrumentation/shiny/usage?last=true"
},
"results": [
{
"content_guid": "bd1d2285-6c80-49af-8a83-a200effe3cb3",
"user_guid": "08e3a41d-1f8e-47f2-8855-f05ea3b0d4b2",
"started": "2018-09-15T18:00:00-05:00",
"ended": "2018-09-15T18:01:00-05:00",
"data_version": 1
}
]
}
135 changes: 135 additions & 0 deletions tests/posit/connect/test_usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import responses

from responses import matchers

from posit.connect import Client
from posit.connect.usage import UsageEvent, rename_params

from .api import load_mock # type: ignore


class TestUsageEventAttributes:
def setup_class(cls):
cls.event = UsageEvent(
None,
None,
**load_mock("v1/instrumentation/shiny/usage?limit=500.json")["results"][0],
)

def test_content_guid(self):
assert self.event.content_guid == "bd1d2285-6c80-49af-8a83-a200effe3cb3"

def test_user_guid(self):
assert self.event.user_guid == "08e3a41d-1f8e-47f2-8855-f05ea3b0d4b2"

def test_started(self):
assert self.event.started == "2018-09-15T18:00:00-05:00"

def test_ended(self):
assert self.event.ended == "2018-09-15T18:01:00-05:00"

def test_data_version(self):
assert self.event.data_version == 1


class TestUsageFind:
@responses.activate
def test(self):
# behavior
mock_get = [None] * 2
mock_get[0] = responses.get(
f"https://connect.example/__api__/v1/instrumentation/shiny/usage",
json=load_mock("v1/instrumentation/shiny/usage?limit=500.json"),
match=[
matchers.query_param_matcher(
{
"limit": 500,
}
)
],
)

mock_get[1] = responses.get(
f"https://connect.example/__api__/v1/instrumentation/shiny/usage",
json=load_mock(
"v1/instrumentation/shiny/usage?limit=500&next=23948901087.json"
),
match=[
matchers.query_param_matcher(
{
"next": "23948901087",
"limit": 500,
}
)
],
)

# setup
c = Client("12345", "https://connect.example")

# invoke
usage = c.usage.find()

# assert
assert mock_get[0].call_count == 1
assert mock_get[1].call_count == 1
assert len(usage) == 1


class TestUsageFindOne:
@responses.activate
def test(self):
# behavior
mock_get = [None] * 2
mock_get[0] = responses.get(
f"https://connect.example/__api__/v1/instrumentation/shiny/usage",
json=load_mock("v1/instrumentation/shiny/usage?limit=500.json"),
match=[
matchers.query_param_matcher(
{
"limit": 500,
}
)
],
)

mock_get[1] = responses.get(
f"https://connect.example/__api__/v1/instrumentation/shiny/usage",
json=load_mock(
"v1/instrumentation/shiny/usage?limit=500&next=23948901087.json"
),
match=[
matchers.query_param_matcher(
{
"next": "23948901087",
"limit": 500,
}
)
],
)

# setup
c = Client("12345", "https://connect.example")

# invoke
event = c.usage.find_one()

# assert
assert mock_get[0].call_count == 1
assert mock_get[1].call_count == 0
assert event
assert event.content_guid == "bd1d2285-6c80-49af-8a83-a200effe3cb3"


class TestRenameParams:
def test_start_to_from(self):
params = {"start": ...}
params = rename_params(params)
assert "start" not in params
assert "from" in params

def test_end_to_to(self):
params = {"end": ...}
params = rename_params(params)
assert "end" not in params
assert "to" in params

0 comments on commit 073c01d

Please sign in to comment.