Skip to content

Commit dd2abf2

Browse files
committed
feat(analytics): add async support for analytics operations
- add async tests for analytics functionality - remove future annotations imports from test files
1 parent c230421 commit dd2abf2

9 files changed

Lines changed: 394 additions & 4 deletions

src/typesense/async_analytics.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Client for Typesense Analytics module (async)."""
2+
3+
from typesense.async_analytics_events import AsyncAnalyticsEvents
4+
from typesense.async_analytics_rules import AsyncAnalyticsRules
5+
from typesense.async_api_call import AsyncApiCall
6+
7+
8+
class AsyncAnalytics:
9+
"""Client for v30 Analytics endpoints (async)."""
10+
11+
def __init__(self, api_call: AsyncApiCall) -> None:
12+
self.api_call = api_call
13+
self.rules = AsyncAnalyticsRules(api_call)
14+
self.events = AsyncAnalyticsEvents(api_call)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Client for Analytics events and status operations (async)."""
2+
3+
import sys
4+
5+
if sys.version_info >= (3, 11):
6+
import typing
7+
else:
8+
import typing_extensions as typing
9+
10+
from typesense.async_api_call import AsyncApiCall
11+
from typesense.types.analytics import (
12+
AnalyticsEvent as AnalyticsEventSchema,
13+
AnalyticsEventCreateResponse,
14+
AnalyticsEventsResponse,
15+
AnalyticsStatus,
16+
)
17+
18+
19+
class AsyncAnalyticsEvents:
20+
events_path: typing.Final[str] = "/analytics/events"
21+
flush_path: typing.Final[str] = "/analytics/flush"
22+
status_path: typing.Final[str] = "/analytics/status"
23+
24+
def __init__(self, api_call: AsyncApiCall) -> None:
25+
self.api_call = api_call
26+
27+
async def create(self, event: AnalyticsEventSchema) -> AnalyticsEventCreateResponse:
28+
response: AnalyticsEventCreateResponse = await self.api_call.post(
29+
AsyncAnalyticsEvents.events_path,
30+
body=event,
31+
as_json=True,
32+
entity_type=AnalyticsEventCreateResponse,
33+
)
34+
return response
35+
36+
async def retrieve(
37+
self,
38+
*,
39+
user_id: str,
40+
name: str,
41+
n: int,
42+
) -> AnalyticsEventsResponse:
43+
params: typing.Dict[str, typing.Union[str, int]] = {
44+
"user_id": user_id,
45+
"name": name,
46+
"n": n,
47+
}
48+
response: AnalyticsEventsResponse = await self.api_call.get(
49+
AsyncAnalyticsEvents.events_path,
50+
params=params,
51+
as_json=True,
52+
entity_type=AnalyticsEventsResponse,
53+
)
54+
return response
55+
56+
async def flush(self) -> AnalyticsEventCreateResponse:
57+
response: AnalyticsEventCreateResponse = await self.api_call.post(
58+
AsyncAnalyticsEvents.flush_path,
59+
body={},
60+
as_json=True,
61+
entity_type=AnalyticsEventCreateResponse,
62+
)
63+
return response
64+
65+
async def status(self) -> AnalyticsStatus:
66+
response: AnalyticsStatus = await self.api_call.get(
67+
AsyncAnalyticsEvents.status_path,
68+
as_json=True,
69+
entity_type=AnalyticsStatus,
70+
)
71+
return response
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Per-rule client for Analytics rules operations (async)."""
2+
3+
from typesense.async_api_call import AsyncApiCall
4+
from typesense.types.analytics import AnalyticsRuleSchema
5+
6+
7+
class AsyncAnalyticsRule:
8+
def __init__(self, api_call: AsyncApiCall, rule_name: str) -> None:
9+
self.api_call = api_call
10+
self.rule_name = rule_name
11+
12+
@property
13+
def _endpoint_path(self) -> str:
14+
from typesense.async_analytics_rules import AsyncAnalyticsRules
15+
16+
return "/".join([AsyncAnalyticsRules.resource_path, self.rule_name])
17+
18+
async def retrieve(self) -> AnalyticsRuleSchema:
19+
response: AnalyticsRuleSchema = await self.api_call.get(
20+
self._endpoint_path,
21+
as_json=True,
22+
entity_type=AnalyticsRuleSchema,
23+
)
24+
return response
25+
26+
async def delete(self) -> AnalyticsRuleSchema:
27+
response: AnalyticsRuleSchema = await self.api_call.delete(
28+
self._endpoint_path,
29+
entity_type=AnalyticsRuleSchema,
30+
)
31+
return response
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Client for Analytics rules collection operations (async)."""
2+
3+
import sys
4+
5+
if sys.version_info >= (3, 11):
6+
import typing
7+
else:
8+
import typing_extensions as typing
9+
10+
from typesense.async_analytics_rule import AsyncAnalyticsRule
11+
from typesense.async_api_call import AsyncApiCall
12+
from typesense.types.analytics import (
13+
AnalyticsRuleCreate,
14+
AnalyticsRuleSchema,
15+
AnalyticsRuleUpdate,
16+
)
17+
18+
19+
class AsyncAnalyticsRules(object):
20+
resource_path: typing.Final[str] = "/analytics/rules"
21+
22+
def __init__(self, api_call: AsyncApiCall) -> None:
23+
self.api_call = api_call
24+
self.rules: typing.Dict[str, AsyncAnalyticsRule] = {}
25+
26+
def __getitem__(self, rule_name: str) -> AsyncAnalyticsRule:
27+
if rule_name not in self.rules:
28+
self.rules[rule_name] = AsyncAnalyticsRule(self.api_call, rule_name)
29+
return self.rules[rule_name]
30+
31+
async def create(self, rule: AnalyticsRuleCreate) -> AnalyticsRuleSchema:
32+
response: AnalyticsRuleSchema = await self.api_call.post(
33+
AsyncAnalyticsRules.resource_path,
34+
body=rule,
35+
as_json=True,
36+
entity_type=AnalyticsRuleSchema,
37+
)
38+
return response
39+
40+
async def retrieve(
41+
self, *, rule_tag: typing.Union[str, None] = None
42+
) -> typing.List[AnalyticsRuleSchema]:
43+
params: typing.Dict[str, str] = {}
44+
if rule_tag:
45+
params["rule_tag"] = rule_tag
46+
response: typing.List[AnalyticsRuleSchema] = await self.api_call.get(
47+
AsyncAnalyticsRules.resource_path,
48+
params=params if params else None,
49+
as_json=True,
50+
entity_type=typing.List[AnalyticsRuleSchema],
51+
)
52+
return response
53+
54+
async def upsert(
55+
self, rule_name: str, update: AnalyticsRuleUpdate
56+
) -> AnalyticsRuleSchema:
57+
response: AnalyticsRuleSchema = await self.api_call.put(
58+
"/".join([AsyncAnalyticsRules.resource_path, rule_name]),
59+
body=update,
60+
entity_type=AnalyticsRuleSchema,
61+
)
62+
return response

tests/analytics_events_test.py

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
"""Tests for Analytics events endpoints (client.analytics.events)."""
22

3-
from __future__ import annotations
4-
53
import pytest
64

75
from tests.utils.version import is_v30_or_above
6+
from typesense.async_analytics_events import AsyncAnalyticsEvents
7+
from typesense.async_analytics_rules import AsyncAnalyticsRules
88
from typesense.client import Client
99
from typesense.types.analytics import AnalyticsEvent
1010

@@ -127,3 +127,82 @@ def test_acutal_retrieve_events(
127127
def test_acutal_flush(actual_client: Client, delete_all: None) -> None:
128128
resp = actual_client.analytics.events.flush()
129129
assert resp["ok"] in [True, False]
130+
131+
132+
async def test_actual_create_event_async(
133+
actual_async_analytics_rules: AsyncAnalyticsRules,
134+
actual_async_analytics_events: AsyncAnalyticsEvents,
135+
delete_all: None,
136+
create_collection: None,
137+
delete_all_analytics_rules: None,
138+
) -> None:
139+
await actual_async_analytics_rules.create(
140+
{
141+
"name": "company_analytics_rule",
142+
"type": "log",
143+
"collection": "companies",
144+
"event_type": "click",
145+
"params": {},
146+
}
147+
)
148+
event: AnalyticsEvent = {
149+
"name": "company_analytics_rule",
150+
"event_type": "query",
151+
"data": {
152+
"user_id": "user-1",
153+
"doc_id": "apple",
154+
},
155+
}
156+
resp = await actual_async_analytics_events.create(event)
157+
assert resp["ok"] is True
158+
await actual_async_analytics_rules["company_analytics_rule"].delete()
159+
160+
161+
async def test_status_async(
162+
actual_async_analytics_events: AsyncAnalyticsEvents,
163+
delete_all: None,
164+
) -> None:
165+
status = await actual_async_analytics_events.status()
166+
assert isinstance(status, dict)
167+
168+
169+
async def test_retrieve_events_async(
170+
actual_async_analytics_rules: AsyncAnalyticsRules,
171+
actual_async_analytics_events: AsyncAnalyticsEvents,
172+
delete_all: None,
173+
create_collection: None,
174+
delete_all_analytics_rules: None,
175+
) -> None:
176+
await actual_async_analytics_rules.create(
177+
{
178+
"name": "company_analytics_rule",
179+
"type": "log",
180+
"collection": "companies",
181+
"event_type": "click",
182+
"params": {},
183+
}
184+
)
185+
event: AnalyticsEvent = {
186+
"name": "company_analytics_rule",
187+
"event_type": "query",
188+
"data": {
189+
"user_id": "user-1",
190+
"doc_id": "apple",
191+
},
192+
}
193+
resp = await actual_async_analytics_events.create(event)
194+
assert resp["ok"] is True
195+
result = await actual_async_analytics_events.retrieve(
196+
user_id="user-1",
197+
name="company_analytics_rule",
198+
n=10,
199+
)
200+
assert "events" in result
201+
202+
203+
async def test_actual_flush_async(
204+
actual_async_analytics_events: AsyncAnalyticsEvents,
205+
delete_all: None,
206+
) -> None:
207+
resp = await actual_async_analytics_events.flush()
208+
assert resp["ok"] in [True, False]

tests/analytics_rule_test.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
"""Unit tests for per-rule AnalyticsRule operations."""
22

3-
from __future__ import annotations
4-
53
import pytest
64

75
from tests.utils.version import is_v30_or_above
86
from typesense.client import Client
97
from typesense.analytics_rule import AnalyticsRule
108
from typesense.analytics_rules import AnalyticsRules
9+
from typesense.async_analytics_rules import AsyncAnalyticsRules
1110

1211

1312
pytestmark = pytest.mark.skipif(
@@ -41,3 +40,23 @@ def test_actual_rule_delete(
4140
) -> None:
4241
resp = actual_analytics_rules["company_analytics_rule"].delete()
4342
assert resp["name"] == "company_analytics_rule"
43+
44+
45+
async def test_actual_rule_retrieve_async(
46+
actual_async_analytics_rules: AsyncAnalyticsRules,
47+
delete_all: None,
48+
delete_all_analytics_rules: None,
49+
create_analytics_rule: None,
50+
) -> None:
51+
resp = await actual_async_analytics_rules["company_analytics_rule"].retrieve()
52+
assert resp["name"] == "company_analytics_rule"
53+
54+
55+
async def test_actual_rule_delete_async(
56+
actual_async_analytics_rules: AsyncAnalyticsRules,
57+
delete_all: None,
58+
delete_all_analytics_rules: None,
59+
create_analytics_rule: None,
60+
) -> None:
61+
resp = await actual_async_analytics_rules["company_analytics_rule"].delete()
62+
assert resp["name"] == "company_analytics_rule"

tests/analytics_rules_test.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from typesense.client import Client
99
from typesense.analytics_rules import AnalyticsRules
1010
from typesense.analytics_rule import AnalyticsRule
11+
from typesense.async_api_call import AsyncApiCall
12+
from typesense.async_analytics_rules import AsyncAnalyticsRules
1113
from typesense.types.analytics import AnalyticsRuleCreate
1214

1315

@@ -82,3 +84,68 @@ def test_actual_retrieve(
8284
rules = actual_analytics_rules.retrieve()
8385
assert isinstance(rules, list)
8486
assert any(r.get("name") == "company_analytics_rule" for r in rules)
87+
88+
89+
def test_rules_init_async(fake_async_api_call: AsyncApiCall) -> None:
90+
from typesense.async_analytics_rules import AsyncAnalyticsRules
91+
92+
rules = AsyncAnalyticsRules(fake_async_api_call)
93+
assert rules.rules == {}
94+
95+
96+
def test_rule_getitem_async(fake_async_api_call: AsyncApiCall) -> None:
97+
from typesense.async_analytics_rules import AsyncAnalyticsRules
98+
from typesense.async_analytics_rule import AsyncAnalyticsRule
99+
100+
rules = AsyncAnalyticsRules(fake_async_api_call)
101+
rule = rules["company_analytics_rule"]
102+
assert isinstance(rule, AsyncAnalyticsRule)
103+
assert rule._endpoint_path == "/analytics/rules/company_analytics_rule"
104+
105+
106+
async def test_actual_create_async(
107+
actual_async_analytics_rules: AsyncAnalyticsRules,
108+
delete_all: None,
109+
delete_all_analytics_rules: None,
110+
create_collection: None,
111+
create_query_collection: None,
112+
) -> None:
113+
body: AnalyticsRuleCreate = {
114+
"name": "company_analytics_rule",
115+
"type": "nohits_queries",
116+
"collection": "companies",
117+
"event_type": "search",
118+
"params": {"destination_collection": "companies_queries", "limit": 1000},
119+
}
120+
resp = await actual_async_analytics_rules.create(rule=body)
121+
assert resp["name"] == "company_analytics_rule"
122+
assert resp["params"]["destination_collection"] == "companies_queries"
123+
124+
125+
async def test_actual_update_async(
126+
actual_async_analytics_rules: AsyncAnalyticsRules,
127+
delete_all: None,
128+
delete_all_analytics_rules: None,
129+
create_analytics_rule: None,
130+
) -> None:
131+
resp = await actual_async_analytics_rules.upsert(
132+
"company_analytics_rule",
133+
{
134+
"params": {
135+
"destination_collection": "companies_queries",
136+
"limit": 500,
137+
},
138+
},
139+
)
140+
assert resp["name"] == "company_analytics_rule"
141+
142+
143+
async def test_actual_retrieve_async(
144+
actual_async_analytics_rules: AsyncAnalyticsRules,
145+
delete_all: None,
146+
delete_all_analytics_rules: None,
147+
create_analytics_rule: None,
148+
) -> None:
149+
rules = await actual_async_analytics_rules.retrieve()
150+
assert isinstance(rules, list)
151+
assert any(r.get("name") == "company_analytics_rule" for r in rules)

0 commit comments

Comments
 (0)