diff --git a/src/posit/connect/client.py b/src/posit/connect/client.py index 1ee0a6c7..58f6144c 100644 --- a/src/posit/connect/client.py +++ b/src/posit/connect/client.py @@ -11,7 +11,7 @@ from .config import Config from .oauth import OAuthIntegration from .content import Content -from .metrics.usage import Usage +from .metrics.shiny_usage import ShinyUsage from .users import User, Users from .metrics.visits import Visits @@ -127,8 +127,8 @@ def metrics(self) -> metrics.Metrics: >>> from posit import connect >>> client = connect.Client() >>> content_guid = "2243770d-ace0-4782-87f9-fe2aeca14fc8" - >>> view_events = client.metrics.views.find(content_guid=content_guid) - >>> len(view_events) + >>> events = client.metrics.usage.find(content_guid=content_guid) + >>> len(events) 24 """ return metrics.Metrics(self.config, self.session) diff --git a/src/posit/connect/metrics/__init__.py b/src/posit/connect/metrics/__init__.py index 392bf31a..fd82f39d 100644 --- a/src/posit/connect/metrics/__init__.py +++ b/src/posit/connect/metrics/__init__.py @@ -1,9 +1,9 @@ from .. import resources -from . import views +from . import usage class Metrics(resources.Resources): @property - def views(self) -> views.Views: - return views.Views(self.config, self.session) + def usage(self) -> usage.Usage: + return usage.Usage(self.config, self.session) diff --git a/src/posit/connect/metrics/shiny_usage.py b/src/posit/connect/metrics/shiny_usage.py new file mode 100644 index 00000000..bd4f06e0 --- /dev/null +++ b/src/posit/connect/metrics/shiny_usage.py @@ -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 ShinyUsageEvent(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 ShinyUsage(Resources): + @overload + def find( + self, + content_guid: str = ..., + min_data_version: int = ..., + start: str = ..., + end: str = ..., + ) -> List[ShinyUsageEvent]: + """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[ShinyUsageEvent] + """ + ... + + @overload + def find(self, *args, **kwargs) -> List[ShinyUsageEvent]: + """Find usage. + + Returns + ------- + List[ShinyUsageEvent] + """ + ... + + def find(self, *args, **kwargs) -> List[ShinyUsageEvent]: + """Find usage. + + Returns + ------- + List[ShinyUsageEvent] + """ + 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 [ + ShinyUsageEvent( + 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 = ..., + ) -> ShinyUsageEvent | 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 + ------- + ShinyUsageEvent | None + """ + ... + + @overload + def find_one(self, *args, **kwargs) -> ShinyUsageEvent | None: + """Find a usage event. + + Returns + ------- + ShinyUsageEvent | None + """ + ... + + def find_one(self, *args, **kwargs) -> ShinyUsageEvent | None: + """Find a usage event. + + Returns + ------- + ShinyUsageEvent | 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 = ( + ShinyUsageEvent( + 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 diff --git a/src/posit/connect/metrics/usage.py b/src/posit/connect/metrics/usage.py index 25c1a716..e9deda27 100644 --- a/src/posit/connect/metrics/usage.py +++ b/src/posit/connect/metrics/usage.py @@ -1,14 +1,66 @@ from __future__ import annotations +import itertools + from typing import List, overload -from .. import urls +from requests.sessions import Session as Session + +from . import shiny_usage, visits + +from .. import resources + + +class UsageEvent(resources.Resource): + @staticmethod + def from_event( + event: visits.VisitEvent | shiny_usage.ShinyUsageEvent, + ) -> UsageEvent: + if type(event) == visits.VisitEvent: + return UsageEvent.from_visit_event(event) + + if type(event) == shiny_usage.ShinyUsageEvent: + return UsageEvent.from_shiny_usage_event(event) -from ..cursors import CursorPaginator -from ..resources import Resource, Resources + raise TypeError + @staticmethod + def from_visit_event(event: visits.VisitEvent) -> UsageEvent: + return UsageEvent( + event.config, + event.session, + content_guid=event.content_guid, + user_guid=event.user_guid, + variant_key=event.variant_key, + rendering_id=event.rendering_id, + bundle_id=event.bundle_id, + started=event.time, + ended=event.time, + data_version=event.data_version, + path=event.path, + ) + + @staticmethod + def from_shiny_usage_event( + event: shiny_usage.ShinyUsageEvent, + ) -> UsageEvent: + return UsageEvent( + event.config, + event.session, + content_guid=event.content_guid, + user_guid=event.user_guid, + variant_key=None, + rendering_id=None, + bundle_id=None, + started=event.started, + ended=event.ended, + data_version=event.data_version, + path=None, + ) + + def __init__(self, config: resources.Config, session: Session, **kwargs): + super().__init__(config, session, **kwargs) -class UsageEvent(Resource): @property def content_guid(self) -> str: """The associated unique content identifier. @@ -29,9 +81,41 @@ def user_guid(self) -> str: """ return self["user_guid"] + @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["variant_key"] + + @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 | None: + """The bundle id associated with the visit. + + Returns + ------- + int + """ + return self["bundle_id"] + @property def started(self) -> str: - """The started timestamp. + """The visit timestamp. Returns ------- @@ -41,7 +125,7 @@ def started(self) -> str: @property def ended(self) -> str: - """The ended timestamp. + """The visit timestamp. Returns ------- @@ -59,8 +143,18 @@ def data_version(self) -> int: """ return self["data_version"] + @property + def path(self) -> str | None: + """The path requested by the user. + + Returns + ------- + str + """ + return self["path"] + -class Usage(Resources): +class Usage(resources.Resources): @overload def find( self, @@ -69,7 +163,7 @@ def find( start: str = ..., end: str = ..., ) -> List[UsageEvent]: - """Find usage. + """Find view events. Parameters ---------- @@ -90,7 +184,7 @@ def find( @overload def find(self, *args, **kwargs) -> List[UsageEvent]: - """Find usage. + """Find view events. Returns ------- @@ -99,27 +193,23 @@ def find(self, *args, **kwargs) -> List[UsageEvent]: ... def find(self, *args, **kwargs) -> List[UsageEvent]: - """Find usage. + """Find view events. 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, + events = [] + finders = (visits.Visits, shiny_usage.ShinyUsage) + for finder in finders: + instance = finder(self.config, self.session) + events.extend( + [ + UsageEvent.from_event(event) + for event in instance.find(*args, **kwargs) # type: ignore[attr-defined] + ] ) - for result in results - ] + return events @overload def find_one( @@ -129,7 +219,7 @@ def find_one( start: str = ..., end: str = ..., ) -> UsageEvent | None: - """Find a usage event. + """Find a view event. Parameters ---------- @@ -144,64 +234,31 @@ def find_one( Returns ------- - UsageEvent | None + Visit | None """ ... @overload def find_one(self, *args, **kwargs) -> UsageEvent | None: - """Find a usage event. + """Find a view event. Returns ------- - UsageEvent | None + Visit | None """ ... def find_one(self, *args, **kwargs) -> UsageEvent | None: - """Find a usage event. + """Find a view 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 + finders = (visits.Visits, shiny_usage.ShinyUsage) + for finder in finders: + instance = finder(self.config, self.session) + event = instance.find_one(*args, **kwargs) # type: ignore[attr-defined] + if event: + return UsageEvent.from_event(event) + return None diff --git a/src/posit/connect/metrics/views.py b/src/posit/connect/metrics/views.py deleted file mode 100644 index a3101ecc..00000000 --- a/src/posit/connect/metrics/views.py +++ /dev/null @@ -1,260 +0,0 @@ -from __future__ import annotations - -import itertools - -from typing import List, overload - -from requests.sessions import Session as Session - -from . import usage, visits - -from .. import resources - - -class ViewEvent(resources.Resource): - @staticmethod - def from_event(event: visits.VisitEvent | usage.UsageEvent) -> ViewEvent: - if type(event) == visits.VisitEvent: - return ViewEvent.from_visit_event(event) - - if type(event) == usage.UsageEvent: - return ViewEvent.from_usage_event(event) - - raise TypeError - - @staticmethod - def from_visit_event(event: visits.VisitEvent) -> ViewEvent: - return ViewEvent( - event.config, - event.session, - content_guid=event.content_guid, - user_guid=event.user_guid, - variant_key=event.variant_key, - rendering_id=event.rendering_id, - bundle_id=event.bundle_id, - started=event.time, - ended=event.time, - data_version=event.data_version, - path=event.path, - ) - - @staticmethod - def from_usage_event(event: usage.UsageEvent) -> ViewEvent: - return ViewEvent( - event.config, - event.session, - content_guid=event.content_guid, - user_guid=event.user_guid, - variant_key=None, - rendering_id=None, - bundle_id=None, - started=event.started, - ended=event.ended, - data_version=event.data_version, - path=None, - ) - - def __init__(self, config: resources.Config, session: Session, **kwargs): - super().__init__(config, session, **kwargs) - - @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 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["variant_key"] - - @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 | None: - """The bundle id associated with the visit. - - Returns - ------- - int - """ - return self["bundle_id"] - - @property - def started(self) -> str: - """The visit timestamp. - - Returns - ------- - str - """ - return self["started"] - - @property - def ended(self) -> str: - """The visit timestamp. - - Returns - ------- - str - """ - return self["ended"] - - @property - def data_version(self) -> int: - """The data version. - - Returns - ------- - int - """ - return self["data_version"] - - @property - def path(self) -> str | None: - """The path requested by the user. - - Returns - ------- - str - """ - return self["path"] - - -class Views(resources.Resources): - @overload - def find( - self, - content_guid: str = ..., - min_data_version: int = ..., - start: str = ..., - end: str = ..., - ) -> List[ViewEvent]: - """Find view events. - - 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[ViewEvent] - """ - ... - - @overload - def find(self, *args, **kwargs) -> List[ViewEvent]: - """Find view events. - - Returns - ------- - List[ViewEvent] - """ - ... - - def find(self, *args, **kwargs) -> List[ViewEvent]: - """Find view events. - - Returns - ------- - List[ViewEvent] - """ - events = [] - finders = (visits.Visits, usage.Usage) - for finder in finders: - instance = finder(self.config, self.session) - events.extend( - [ - ViewEvent.from_event(event) - for event in instance.find(*args, **kwargs) # type: ignore[attr-defined] - ] - ) - return events - - @overload - def find_one( - self, - content_guid: str = ..., - min_data_version: int = ..., - start: str = ..., - end: str = ..., - ) -> ViewEvent | None: - """Find a view 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 - ------- - Visit | None - """ - ... - - @overload - def find_one(self, *args, **kwargs) -> ViewEvent | None: - """Find a view event. - - Returns - ------- - Visit | None - """ - ... - - def find_one(self, *args, **kwargs) -> ViewEvent | None: - """Find a view event. - - Returns - ------- - ViewEvent | None - """ - finders = (visits.Visits, usage.Usage) - for finder in finders: - instance = finder(self.config, self.session) - event = instance.find_one(*args, **kwargs) # type: ignore[attr-defined] - if event: - return ViewEvent.from_event(event) - return None diff --git a/tests/posit/connect/metrics/test_shiny_usage.py b/tests/posit/connect/metrics/test_shiny_usage.py new file mode 100644 index 00000000..111ec236 --- /dev/null +++ b/tests/posit/connect/metrics/test_shiny_usage.py @@ -0,0 +1,142 @@ +import responses +import requests + +from responses import matchers + +from posit.connect import Client, config +from posit.connect.metrics import shiny_usage + +from ..api import load_mock # type: ignore + + +class TestShinyUsageEventAttributes: + def setup_class(cls): + cls.event = shiny_usage.ShinyUsageEvent( + 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 TestShinyUsageFind: + @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 = config.Config("12345", "https://connect.example") + session = requests.Session() + + # invoke + events = shiny_usage.ShinyUsage(c, session).find() + + # assert + assert mock_get[0].call_count == 1 + assert mock_get[1].call_count == 1 + assert len(events) == 1 + + +class TestShinyUsageFindOne: + @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 = config.Config("12345", "https://connect.example") + session = requests.Session() + + # invoke + event = shiny_usage.ShinyUsage(c, session).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 = shiny_usage.rename_params(params) + assert "start" not in params + assert "from" in params + + def test_end_to_to(self): + params = {"end": ...} + params = shiny_usage.rename_params(params) + assert "end" not in params + assert "to" in params diff --git a/tests/posit/connect/metrics/test_usage.py b/tests/posit/connect/metrics/test_usage.py index f5b1fa91..e056b513 100644 --- a/tests/posit/connect/metrics/test_usage.py +++ b/tests/posit/connect/metrics/test_usage.py @@ -1,48 +1,143 @@ +import pytest import responses -import requests from responses import matchers -from posit.connect import Client, config -from posit.connect.metrics import usage +from posit import connect +from posit.connect.metrics import shiny_usage, usage, visits + from ..api import load_mock # type: ignore -class TestUsageEventAttributes: +class TestUsageEventFromEvent: + def test(self): + with pytest.raises(TypeError): + usage.UsageEvent.from_event(None) + + +class TestUsageEventFromVisitEvent: + def setup_class(cls): + visit_event = visits.VisitEvent( + None, + None, + **load_mock("v1/instrumentation/content/visits?limit=500.json")[ + "results" + ][0], + ) + cls.view_event = usage.UsageEvent.from_visit_event(visit_event) + + def test_content_guid(self): + assert ( + self.view_event.content_guid + == "bd1d2285-6c80-49af-8a83-a200effe3cb3" + ) + + def test_user_guid(self): + assert ( + self.view_event.user_guid == "08e3a41d-1f8e-47f2-8855-f05ea3b0d4b2" + ) + + def test_variant_key(self): + assert self.view_event.variant_key == "HidI2Kwq" + + def test_rendering_id(self): + assert self.view_event.rendering_id == 7 + + def test_bundle_id(self): + assert self.view_event.bundle_id == 33 + + def test_started(self): + assert self.view_event.started == "2018-09-15T18:00:00-05:00" + + def test_ended(self): + assert self.view_event.ended == "2018-09-15T18:00:00-05:00" + + def test_data_version(self): + assert self.view_event.data_version == 1 + + def test_path(self): + assert self.view_event.path == "/logs" + + +class TestUsageEventFromShinyUsageEvent: def setup_class(cls): - cls.event = usage.UsageEvent( + visit_event = shiny_usage.ShinyUsageEvent( None, None, **load_mock("v1/instrumentation/shiny/usage?limit=500.json")[ "results" ][0], ) + cls.view_event = usage.UsageEvent.from_shiny_usage_event(visit_event) def test_content_guid(self): assert ( - self.event.content_guid == "bd1d2285-6c80-49af-8a83-a200effe3cb3" + self.view_event.content_guid + == "bd1d2285-6c80-49af-8a83-a200effe3cb3" ) def test_user_guid(self): - assert self.event.user_guid == "08e3a41d-1f8e-47f2-8855-f05ea3b0d4b2" + assert ( + self.view_event.user_guid == "08e3a41d-1f8e-47f2-8855-f05ea3b0d4b2" + ) + + def test_variant_key(self): + assert self.view_event.variant_key is None + + def test_rendering_id(self): + assert self.view_event.rendering_id is None + + def test_bundle_id(self): + assert self.view_event.bundle_id is None def test_started(self): - assert self.event.started == "2018-09-15T18:00:00-05:00" + assert self.view_event.started == "2018-09-15T18:00:00-05:00" def test_ended(self): - assert self.event.ended == "2018-09-15T18:01:00-05:00" + assert self.view_event.ended == "2018-09-15T18:01:00-05:00" def test_data_version(self): - assert self.event.data_version == 1 + assert self.view_event.data_version == 1 + + def test_path(self): + assert self.view_event.path is None class TestUsageFind: @responses.activate def test(self): # behavior - mock_get = [None] * 2 + mock_get = [None] * 4 + mock_get[0] = responses.get( + f"https://connect.example/__api__/v1/instrumentation/content/visits", + json=load_mock("v1/instrumentation/content/visits?limit=500.json"), + match=[ + matchers.query_param_matcher( + { + "limit": 500, + } + ) + ], + ) + + mock_get[1] = responses.get( + f"https://connect.example/__api__/v1/instrumentation/content/visits", + json=load_mock( + "v1/instrumentation/content/visits?limit=500&next=23948901087.json" + ), + match=[ + matchers.query_param_matcher( + { + "next": "23948901087", + "limit": 500, + } + ) + ], + ) + + mock_get[2] = responses.get( f"https://connect.example/__api__/v1/instrumentation/shiny/usage", json=load_mock("v1/instrumentation/shiny/usage?limit=500.json"), match=[ @@ -54,7 +149,7 @@ def test(self): ], ) - mock_get[1] = responses.get( + mock_get[3] = responses.get( f"https://connect.example/__api__/v1/instrumentation/shiny/usage", json=load_mock( "v1/instrumentation/shiny/usage?limit=500&next=23948901087.json" @@ -70,24 +165,53 @@ def test(self): ) # setup - c = config.Config("12345", "https://connect.example") - session = requests.Session() + c = connect.Client("12345", "https://connect.example") # invoke - events = usage.Usage(c, session).find() + events = c.metrics.usage.find() # assert assert mock_get[0].call_count == 1 assert mock_get[1].call_count == 1 - assert len(events) == 1 + assert mock_get[2].call_count == 1 + assert mock_get[3].call_count == 1 + assert len(events) == 2 class TestUsageFindOne: @responses.activate def test(self): # behavior - mock_get = [None] * 2 + mock_get = [None] * 4 + mock_get[0] = responses.get( + f"https://connect.example/__api__/v1/instrumentation/content/visits", + json=load_mock("v1/instrumentation/content/visits?limit=500.json"), + match=[ + matchers.query_param_matcher( + { + "limit": 500, + } + ) + ], + ) + + mock_get[1] = responses.get( + f"https://connect.example/__api__/v1/instrumentation/content/visits", + json=load_mock( + "v1/instrumentation/content/visits?limit=500&next=23948901087.json" + ), + match=[ + matchers.query_param_matcher( + { + "next": "23948901087", + "limit": 500, + } + ) + ], + ) + + mock_get[2] = responses.get( f"https://connect.example/__api__/v1/instrumentation/shiny/usage", json=load_mock("v1/instrumentation/shiny/usage?limit=500.json"), match=[ @@ -99,7 +223,7 @@ def test(self): ], ) - mock_get[1] = responses.get( + mock_get[3] = responses.get( f"https://connect.example/__api__/v1/instrumentation/shiny/usage", json=load_mock( "v1/instrumentation/shiny/usage?limit=500&next=23948901087.json" @@ -115,28 +239,48 @@ def test(self): ) # setup - c = config.Config("12345", "https://connect.example") - session = requests.Session() + c = connect.Client("12345", "https://connect.example") # invoke - event = usage.Usage(c, session).find_one() + view_event = c.metrics.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 = usage.rename_params(params) - assert "start" not in params - assert "from" in params - - def test_end_to_to(self): - params = {"end": ...} - params = usage.rename_params(params) - assert "end" not in params - assert "to" in params + assert mock_get[2].call_count == 0 + assert mock_get[3].call_count == 0 + assert view_event + assert ( + view_event.content_guid == "bd1d2285-6c80-49af-8a83-a200effe3cb3" + ) + + @responses.activate + def test_none(self): + # behavior + mock_get = [None] * 2 + + # return an empty result set to push through the iterator + mock_get[0] = responses.get( + f"https://connect.example/__api__/v1/instrumentation/content/visits", + json=load_mock( + "v1/instrumentation/content/visits?limit=500&next=23948901087.json" + ), + ) + + 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" + ), + ) + + # setup + c = connect.Client("12345", "https://connect.example") + + # invoke + view_event = c.metrics.usage.find_one(content_guid="not-found") + + # assert + assert mock_get[0].call_count == 1 + assert mock_get[1].call_count == 1 + assert view_event is None diff --git a/tests/posit/connect/metrics/test_views.py b/tests/posit/connect/metrics/test_views.py deleted file mode 100644 index c83094d1..00000000 --- a/tests/posit/connect/metrics/test_views.py +++ /dev/null @@ -1,286 +0,0 @@ -import pytest -import responses - -from responses import matchers - -from posit import connect -from posit.connect.metrics import views, visits, usage - - -from ..api import load_mock # type: ignore - - -class TestViewEventFromEvent: - def test(self): - with pytest.raises(TypeError): - views.ViewEvent.from_event(None) - - -class TestViewEventFromVisitEvent: - def setup_class(cls): - visit_event = visits.VisitEvent( - None, - None, - **load_mock("v1/instrumentation/content/visits?limit=500.json")[ - "results" - ][0], - ) - cls.view_event = views.ViewEvent.from_visit_event(visit_event) - - def test_content_guid(self): - assert ( - self.view_event.content_guid - == "bd1d2285-6c80-49af-8a83-a200effe3cb3" - ) - - def test_user_guid(self): - assert ( - self.view_event.user_guid == "08e3a41d-1f8e-47f2-8855-f05ea3b0d4b2" - ) - - def test_variant_key(self): - assert self.view_event.variant_key == "HidI2Kwq" - - def test_rendering_id(self): - assert self.view_event.rendering_id == 7 - - def test_bundle_id(self): - assert self.view_event.bundle_id == 33 - - def test_started(self): - assert self.view_event.started == "2018-09-15T18:00:00-05:00" - - def test_ended(self): - assert self.view_event.ended == "2018-09-15T18:00:00-05:00" - - def test_data_version(self): - assert self.view_event.data_version == 1 - - def test_path(self): - assert self.view_event.path == "/logs" - - -class TestViewEventFromUsageEvent: - def setup_class(cls): - visit_event = usage.UsageEvent( - None, - None, - **load_mock("v1/instrumentation/shiny/usage?limit=500.json")[ - "results" - ][0], - ) - cls.view_event = views.ViewEvent.from_usage_event(visit_event) - - def test_content_guid(self): - assert ( - self.view_event.content_guid - == "bd1d2285-6c80-49af-8a83-a200effe3cb3" - ) - - def test_user_guid(self): - assert ( - self.view_event.user_guid == "08e3a41d-1f8e-47f2-8855-f05ea3b0d4b2" - ) - - def test_variant_key(self): - assert self.view_event.variant_key is None - - def test_rendering_id(self): - assert self.view_event.rendering_id is None - - def test_bundle_id(self): - assert self.view_event.bundle_id is None - - def test_started(self): - assert self.view_event.started == "2018-09-15T18:00:00-05:00" - - def test_ended(self): - assert self.view_event.ended == "2018-09-15T18:01:00-05:00" - - def test_data_version(self): - assert self.view_event.data_version == 1 - - def test_path(self): - assert self.view_event.path is None - - -class TestViewsFind: - @responses.activate - def test(self): - # behavior - mock_get = [None] * 4 - - mock_get[0] = responses.get( - f"https://connect.example/__api__/v1/instrumentation/content/visits", - json=load_mock("v1/instrumentation/content/visits?limit=500.json"), - match=[ - matchers.query_param_matcher( - { - "limit": 500, - } - ) - ], - ) - - mock_get[1] = responses.get( - f"https://connect.example/__api__/v1/instrumentation/content/visits", - json=load_mock( - "v1/instrumentation/content/visits?limit=500&next=23948901087.json" - ), - match=[ - matchers.query_param_matcher( - { - "next": "23948901087", - "limit": 500, - } - ) - ], - ) - - mock_get[2] = 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[3] = 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 = connect.Client("12345", "https://connect.example") - - # invoke - events = c.metrics.views.find() - - # assert - assert mock_get[0].call_count == 1 - assert mock_get[1].call_count == 1 - assert mock_get[2].call_count == 1 - assert mock_get[3].call_count == 1 - assert len(events) == 2 - - -class TestViewsFindOne: - @responses.activate - def test(self): - # behavior - mock_get = [None] * 4 - - mock_get[0] = responses.get( - f"https://connect.example/__api__/v1/instrumentation/content/visits", - json=load_mock("v1/instrumentation/content/visits?limit=500.json"), - match=[ - matchers.query_param_matcher( - { - "limit": 500, - } - ) - ], - ) - - mock_get[1] = responses.get( - f"https://connect.example/__api__/v1/instrumentation/content/visits", - json=load_mock( - "v1/instrumentation/content/visits?limit=500&next=23948901087.json" - ), - match=[ - matchers.query_param_matcher( - { - "next": "23948901087", - "limit": 500, - } - ) - ], - ) - - mock_get[2] = 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[3] = 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 = connect.Client("12345", "https://connect.example") - - # invoke - view_event = c.metrics.views.find_one() - - # assert - assert mock_get[0].call_count == 1 - assert mock_get[1].call_count == 0 - assert mock_get[2].call_count == 0 - assert mock_get[3].call_count == 0 - assert view_event - assert ( - view_event.content_guid == "bd1d2285-6c80-49af-8a83-a200effe3cb3" - ) - - @responses.activate - def test_none(self): - # behavior - mock_get = [None] * 2 - - # return an empty result set to push through the iterator - mock_get[0] = responses.get( - f"https://connect.example/__api__/v1/instrumentation/content/visits", - json=load_mock( - "v1/instrumentation/content/visits?limit=500&next=23948901087.json" - ), - ) - - 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" - ), - ) - - # setup - c = connect.Client("12345", "https://connect.example") - - # invoke - view_event = c.metrics.views.find_one(content_guid="not-found") - - # assert - assert mock_get[0].call_count == 1 - assert mock_get[1].call_count == 1 - assert view_event is None