trodo-python 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
trodo/__init__.py ADDED
@@ -0,0 +1,134 @@
1
+ """
2
+ trodo-python — Trodo Analytics SDK for Python
3
+
4
+ Usage (module-level singleton):
5
+ import trodo
6
+ trodo.init(site_id='your-site-id')
7
+ user = trodo.for_user('user-123')
8
+ user.track('purchase_completed', {'amount': 99.99})
9
+
10
+ Usage (class):
11
+ from trodo import TrodoClient
12
+ client = TrodoClient(site_id='your-site-id')
13
+ user = client.for_user('user-123')
14
+ user.track('purchase_completed', {'amount': 99.99})
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Any, Dict, List, Optional, Union
20
+
21
+ from .client import TrodoClient
22
+ from .user_context import UserContext
23
+ from .managers.group_manager import GroupProfile
24
+ from .types import ApiResult, IdentifyResult, ResetResult, WalletAddressResult
25
+
26
+ __all__ = [
27
+ "TrodoClient",
28
+ "UserContext",
29
+ "GroupProfile",
30
+ "init",
31
+ "for_user",
32
+ "track",
33
+ "identify",
34
+ "wallet_address",
35
+ "reset",
36
+ "enable_auto_events",
37
+ "disable_auto_events",
38
+ "flush",
39
+ "shutdown",
40
+ ]
41
+
42
+ # ============================================================================
43
+ # Singleton convenience API
44
+ # ============================================================================
45
+
46
+ _client: Optional[TrodoClient] = None
47
+
48
+
49
+ def _get_client() -> TrodoClient:
50
+ if _client is None:
51
+ raise RuntimeError(
52
+ "trodo-python: Call trodo.init(site_id=...) before using the SDK."
53
+ )
54
+ return _client
55
+
56
+
57
+ def init(
58
+ site_id: str,
59
+ api_base: str = "https://sdkapi.trodo.ai",
60
+ timeout: int = 10,
61
+ retries: int = 2,
62
+ batch_enabled: bool = False,
63
+ batch_size: int = 50,
64
+ batch_flush_interval: float = 5.0,
65
+ auto_events: bool = False,
66
+ on_error: Optional[Any] = None,
67
+ debug: bool = False,
68
+ ) -> TrodoClient:
69
+ """Initialise the singleton SDK instance."""
70
+ global _client
71
+ _client = TrodoClient(
72
+ site_id=site_id,
73
+ api_base=api_base,
74
+ timeout=timeout,
75
+ retries=retries,
76
+ batch_enabled=batch_enabled,
77
+ batch_size=batch_size,
78
+ batch_flush_interval=batch_flush_interval,
79
+ auto_events=auto_events,
80
+ on_error=on_error,
81
+ debug=debug,
82
+ )
83
+ return _client
84
+
85
+
86
+ def for_user(
87
+ distinct_id: str,
88
+ session_id: Optional[str] = None,
89
+ ) -> UserContext:
90
+ """Return a UserContext bound to the given distinctId."""
91
+ return _get_client().for_user(distinct_id, session_id)
92
+
93
+
94
+ def track(
95
+ distinct_id: str,
96
+ event_name: str,
97
+ properties: Optional[Dict[str, Any]] = None,
98
+ category: str = "custom",
99
+ ) -> None:
100
+ """Track an event for a user (direct-call pattern)."""
101
+ _get_client().track(distinct_id, event_name, properties, category)
102
+
103
+
104
+ def identify(distinct_id: str, identify_id: str) -> IdentifyResult:
105
+ """Alias a user's distinctId to an external identifier."""
106
+ return _get_client().identify(distinct_id, identify_id)
107
+
108
+
109
+ def wallet_address(distinct_id: str, wallet_addr: str) -> WalletAddressResult:
110
+ """Associate a wallet address with a user."""
111
+ return _get_client().wallet_address(distinct_id, wallet_addr)
112
+
113
+
114
+ def reset(distinct_id: str) -> ResetResult:
115
+ """Reset a user's session."""
116
+ return _get_client().reset(distinct_id)
117
+
118
+
119
+ def enable_auto_events() -> None:
120
+ _get_client().enable_auto_events()
121
+
122
+
123
+ def disable_auto_events() -> None:
124
+ _get_client().disable_auto_events()
125
+
126
+
127
+ def flush() -> None:
128
+ """Flush any queued batch events."""
129
+ _get_client().flush()
130
+
131
+
132
+ def shutdown() -> None:
133
+ """Flush, stop timers, and disable auto events."""
134
+ _get_client().shutdown()
trodo/api/__init__.py ADDED
File without changes
@@ -0,0 +1,96 @@
1
+ """Async HTTP client using httpx (optional dependency)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ import asyncio
7
+ from typing import Any, Callable, Dict, Optional
8
+
9
+ from ..types import ApiResult, EventPayload
10
+
11
+
12
+ class AsyncHttpClient:
13
+ def __init__(
14
+ self,
15
+ api_base: str,
16
+ site_id: str,
17
+ timeout: int = 10,
18
+ retries: int = 2,
19
+ on_error: Optional[Callable[[Exception], None]] = None,
20
+ debug: bool = False,
21
+ ) -> None:
22
+ try:
23
+ import httpx # noqa: F401
24
+ except ImportError:
25
+ raise ImportError(
26
+ "trodo-python async support requires httpx: pip install trodo-python[async]"
27
+ )
28
+
29
+ self.api_base = api_base.rstrip("/")
30
+ self.site_id = site_id
31
+ self.timeout = timeout
32
+ self.retries = retries
33
+ self.on_error = on_error
34
+ self.debug = debug
35
+
36
+ def _log(self, *args: Any) -> None:
37
+ if self.debug:
38
+ sys.stderr.write(f"[trodo-python-async] {' '.join(str(a) for a in args)}\n")
39
+
40
+ async def _request(
41
+ self, path: str, body: Dict[str, Any], attempt: int = 0
42
+ ) -> ApiResult:
43
+ import httpx
44
+
45
+ url = f"{self.api_base}{path}"
46
+ self._log(f"POST {url}")
47
+ headers = {
48
+ "Content-Type": "application/json",
49
+ "X-Trodo-Site-Id": self.site_id,
50
+ }
51
+ try:
52
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
53
+ resp = await client.post(url, json=body, headers=headers)
54
+ if resp.status_code >= 500 and attempt < self.retries:
55
+ delay = 2 ** attempt
56
+ self._log(f"Retry {attempt + 1} after {delay}s")
57
+ await asyncio.sleep(delay)
58
+ return await self._request(path, body, attempt + 1)
59
+ try:
60
+ return resp.json()
61
+ except Exception:
62
+ return {}
63
+ except Exception as exc:
64
+ if attempt < self.retries:
65
+ await asyncio.sleep(2 ** attempt)
66
+ return await self._request(path, body, attempt + 1)
67
+ self._log(f"Error: {exc}")
68
+ if self.on_error:
69
+ self.on_error(exc)
70
+ return {}
71
+
72
+ async def post_track(self, session_data: Dict[str, Any]) -> ApiResult:
73
+ return await self._request("/api/sdk/track", {"sessionData": session_data})
74
+
75
+ async def post_event(self, event: EventPayload) -> ApiResult:
76
+ return await self._request("/api/events", event.to_dict())
77
+
78
+ async def post_bulk_events(self, events: list) -> ApiResult:
79
+ return await self._request(
80
+ "/api/events/bulk", {"events": [e.to_dict() for e in events]}
81
+ )
82
+
83
+ async def post_identify(self, payload: Dict[str, Any]) -> ApiResult:
84
+ return await self._request("/api/sdk/identify", payload)
85
+
86
+ async def post_wallet_address(self, payload: Dict[str, Any]) -> ApiResult:
87
+ return await self._request("/api/sdk/wallet-address", payload)
88
+
89
+ async def post_reset(self, payload: Dict[str, Any]) -> ApiResult:
90
+ return await self._request("/api/sdk/reset", payload)
91
+
92
+ async def post_people(self, path: str, payload: Dict[str, Any]) -> ApiResult:
93
+ return await self._request(path, payload)
94
+
95
+ async def post_group(self, path: str, payload: Dict[str, Any]) -> ApiResult:
96
+ return await self._request(path, payload)
trodo/api/endpoints.py ADDED
@@ -0,0 +1,20 @@
1
+ TRACK = "/api/sdk/track"
2
+ EVENTS = "/api/events"
3
+ EVENTS_BULK = "/api/events/bulk"
4
+ IDENTIFY = "/api/sdk/identify"
5
+ WALLET_ADDRESS = "/api/sdk/wallet-address"
6
+ RESET = "/api/sdk/reset"
7
+ PEOPLE_SET = "/api/sdk/people/set"
8
+ PEOPLE_SET_ONCE = "/api/sdk/people/set_once"
9
+ PEOPLE_UNSET = "/api/sdk/people/unset"
10
+ PEOPLE_INCREMENT = "/api/sdk/people/increment"
11
+ PEOPLE_APPEND = "/api/sdk/people/append"
12
+ PEOPLE_UNION = "/api/sdk/people/union"
13
+ PEOPLE_REMOVE = "/api/sdk/people/remove"
14
+ PEOPLE_TRACK_CHARGE = "/api/sdk/people/track_charge"
15
+ PEOPLE_CLEAR_CHARGES = "/api/sdk/people/clear_charges"
16
+ PEOPLE_DELETE_USER = "/api/sdk/people/delete_user"
17
+ GROUPS_SET = "/api/sdk/groups/set_group"
18
+ GROUPS_ADD = "/api/sdk/groups/add_group"
19
+ GROUPS_REMOVE = "/api/sdk/groups/remove_group"
20
+ GROUPS_PROFILE = "/api/sdk/groups/profile"
@@ -0,0 +1,87 @@
1
+ """Synchronous HTTP client using requests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ import time
7
+ from typing import Any, Callable, Dict, Optional
8
+
9
+ import requests
10
+
11
+ from ..types import ApiResult, EventPayload
12
+
13
+
14
+ class HttpClient:
15
+ def __init__(
16
+ self,
17
+ api_base: str,
18
+ site_id: str,
19
+ timeout: int = 10,
20
+ retries: int = 2,
21
+ on_error: Optional[Callable[[Exception], None]] = None,
22
+ debug: bool = False,
23
+ ) -> None:
24
+ self.api_base = api_base.rstrip("/")
25
+ self.site_id = site_id
26
+ self.timeout = timeout
27
+ self.retries = retries
28
+ self.on_error = on_error
29
+ self.debug = debug
30
+ self._session = requests.Session()
31
+ self._session.headers.update({
32
+ "Content-Type": "application/json",
33
+ "X-Trodo-Site-Id": self.site_id,
34
+ })
35
+
36
+ def _log(self, *args: Any) -> None:
37
+ if self.debug:
38
+ sys.stderr.write(f"[trodo-python] {' '.join(str(a) for a in args)}\n")
39
+
40
+ def _request(self, path: str, body: Dict[str, Any], attempt: int = 0) -> ApiResult:
41
+ url = f"{self.api_base}{path}"
42
+ self._log(f"POST {url}")
43
+ try:
44
+ resp = self._session.post(url, json=body, timeout=self.timeout)
45
+ if resp.status_code >= 500 and attempt < self.retries:
46
+ delay = 2 ** attempt
47
+ self._log(f"Retry {attempt + 1} after {delay}s (status {resp.status_code})")
48
+ time.sleep(delay)
49
+ return self._request(path, body, attempt + 1)
50
+ try:
51
+ return resp.json()
52
+ except Exception:
53
+ return {}
54
+ except Exception as exc:
55
+ if attempt < self.retries:
56
+ delay = 2 ** attempt
57
+ self._log(f"Retry {attempt + 1} after {delay}s (network error)")
58
+ time.sleep(delay)
59
+ return self._request(path, body, attempt + 1)
60
+ self._log(f"Error: {exc}")
61
+ if self.on_error:
62
+ self.on_error(exc)
63
+ return {}
64
+
65
+ def post_track(self, session_data: Dict[str, Any]) -> ApiResult:
66
+ return self._request("/api/sdk/track", {"sessionData": session_data})
67
+
68
+ def post_event(self, event: EventPayload) -> ApiResult:
69
+ return self._request("/api/events", event.to_dict())
70
+
71
+ def post_bulk_events(self, events: list) -> ApiResult:
72
+ return self._request("/api/events/bulk", {"events": [e.to_dict() for e in events]})
73
+
74
+ def post_identify(self, payload: Dict[str, Any]) -> ApiResult:
75
+ return self._request("/api/sdk/identify", payload)
76
+
77
+ def post_wallet_address(self, payload: Dict[str, Any]) -> ApiResult:
78
+ return self._request("/api/sdk/wallet-address", payload)
79
+
80
+ def post_reset(self, payload: Dict[str, Any]) -> ApiResult:
81
+ return self._request("/api/sdk/reset", payload)
82
+
83
+ def post_people(self, path: str, payload: Dict[str, Any]) -> ApiResult:
84
+ return self._request(path, payload)
85
+
86
+ def post_group(self, path: str, payload: Dict[str, Any]) -> ApiResult:
87
+ return self._request(path, payload)
trodo/auto/__init__.py ADDED
File without changes
@@ -0,0 +1,134 @@
1
+ """Auto event manager — captures unhandled Python exceptions as server_error events."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ import threading
7
+ import traceback
8
+ from datetime import datetime, timezone
9
+ from typing import Any, Optional, TYPE_CHECKING
10
+
11
+ from ..types import EventPayload
12
+
13
+ if TYPE_CHECKING:
14
+ from ..session.session_manager import SessionManager
15
+ from ..api.http_client import HttpClient
16
+
17
+
18
+ def _now_iso() -> str:
19
+ return datetime.now(timezone.utc).isoformat()
20
+
21
+
22
+ class AutoEventManager:
23
+ def __init__(
24
+ self,
25
+ site_id: str,
26
+ http_client: "HttpClient",
27
+ session_manager: "SessionManager",
28
+ ) -> None:
29
+ self._site_id = site_id
30
+ self._http = http_client
31
+ self._session_manager = session_manager
32
+ self._enabled = False
33
+ self._prev_excepthook: Any = None
34
+ self._prev_thread_excepthook: Any = None
35
+
36
+ def enable(self) -> None:
37
+ if self._enabled:
38
+ return
39
+ self._enabled = True
40
+
41
+ # Wrap sys.excepthook
42
+ self._prev_excepthook = sys.excepthook
43
+
44
+ def _excepthook(exc_type, exc_value, exc_tb):
45
+ self._track_error(
46
+ exc_type.__name__ if exc_type else "Exception",
47
+ str(exc_value),
48
+ "".join(traceback.format_exception(exc_type, exc_value, exc_tb)),
49
+ )
50
+ if self._prev_excepthook:
51
+ self._prev_excepthook(exc_type, exc_value, exc_tb)
52
+
53
+ sys.excepthook = _excepthook
54
+
55
+ # Wrap threading.excepthook (Python 3.8+)
56
+ if hasattr(threading, "excepthook"):
57
+ self._prev_thread_excepthook = threading.excepthook
58
+
59
+ def _thread_excepthook(args):
60
+ self._track_error(
61
+ args.exc_type.__name__ if args.exc_type else "ThreadException",
62
+ str(args.exc_value),
63
+ "".join(
64
+ traceback.format_exception(
65
+ args.exc_type, args.exc_value, args.exc_tb
66
+ )
67
+ ),
68
+ )
69
+ if self._prev_thread_excepthook:
70
+ self._prev_thread_excepthook(args)
71
+
72
+ threading.excepthook = _thread_excepthook
73
+
74
+ def disable(self) -> None:
75
+ if not self._enabled:
76
+ return
77
+ self._enabled = False
78
+
79
+ if self._prev_excepthook is not None:
80
+ sys.excepthook = self._prev_excepthook
81
+ self._prev_excepthook = None
82
+
83
+ if hasattr(threading, "excepthook") and self._prev_thread_excepthook is not None:
84
+ threading.excepthook = self._prev_thread_excepthook
85
+ self._prev_thread_excepthook = None
86
+
87
+ def is_enabled(self) -> bool:
88
+ return self._enabled
89
+
90
+ def track_error(
91
+ self,
92
+ error: Exception,
93
+ error_type: Optional[str] = None,
94
+ severity: str = "error",
95
+ distinct_id: str = "server_global",
96
+ ) -> None:
97
+ self._track_error(
98
+ error_type or type(error).__name__,
99
+ str(error),
100
+ traceback.format_exc() or repr(error),
101
+ severity=severity,
102
+ distinct_id=distinct_id,
103
+ )
104
+
105
+ def _track_error(
106
+ self,
107
+ error_type: str,
108
+ message: str,
109
+ stack: str,
110
+ severity: str = "critical",
111
+ distinct_id: str = "server_global",
112
+ ) -> None:
113
+ try:
114
+ session = self._session_manager.get_or_create(distinct_id, self._site_id)
115
+ self._session_manager.ensure_confirmed(session, self._http)
116
+
117
+ event = EventPayload(
118
+ event_type="auto",
119
+ event_name="server_error",
120
+ event_category="error",
121
+ session_id=session.session_id,
122
+ user_id=session.distinct_id,
123
+ custom_properties={
124
+ "error_type": error_type,
125
+ "message": message,
126
+ "stack": stack,
127
+ "runtime": "python",
128
+ "severity": severity,
129
+ "timestamp": _now_iso(),
130
+ },
131
+ )
132
+ self._http.post_event(event)
133
+ except Exception:
134
+ pass # Never let error tracking crash the process
trodo/client.py ADDED
@@ -0,0 +1,195 @@
1
+ """TrodoClient — main SDK class for Python."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, List, Optional, Union
6
+
7
+ from .api.http_client import HttpClient
8
+ from .session.session_manager import SessionManager
9
+ from .managers.people_manager import PeopleManager
10
+ from .managers.group_manager import GroupManager, GroupProfile
11
+ from .queue.event_queue import EventQueue
12
+ from .queue.batch_flusher import BatchFlusher
13
+ from .auto.auto_event_manager import AutoEventManager
14
+ from .user_context import UserContext
15
+ from .types import (
16
+ ApiResult,
17
+ IdentifyResult,
18
+ ResetResult,
19
+ WalletAddressResult,
20
+ )
21
+
22
+
23
+ class TrodoClient:
24
+ def __init__(
25
+ self,
26
+ site_id: str,
27
+ api_base: str = "https://sdkapi.trodo.ai",
28
+ timeout: int = 10,
29
+ retries: int = 2,
30
+ batch_enabled: bool = False,
31
+ batch_size: int = 50,
32
+ batch_flush_interval: float = 5.0,
33
+ auto_events: bool = False,
34
+ on_error: Optional[Any] = None,
35
+ debug: bool = False,
36
+ ) -> None:
37
+ if not site_id:
38
+ raise ValueError("trodo-python: site_id is required")
39
+
40
+ self.site_id = site_id
41
+
42
+ self._http = HttpClient(
43
+ api_base=api_base,
44
+ site_id=site_id,
45
+ timeout=timeout,
46
+ retries=retries,
47
+ on_error=on_error,
48
+ debug=debug,
49
+ )
50
+
51
+ self._session_manager = SessionManager()
52
+
53
+ if batch_enabled:
54
+ self._event_queue: Optional[EventQueue] = EventQueue(batch_size)
55
+ self._batch_flusher: Optional[BatchFlusher] = BatchFlusher(
56
+ self._event_queue, self._http, batch_flush_interval
57
+ )
58
+ self._batch_flusher.start()
59
+ else:
60
+ self._event_queue = None
61
+ self._batch_flusher = None
62
+
63
+ self._auto_event_manager = AutoEventManager(
64
+ site_id, self._http, self._session_manager
65
+ )
66
+
67
+ if auto_events:
68
+ self._auto_event_manager.enable()
69
+
70
+ self._user_context_cache: Dict[str, UserContext] = {}
71
+
72
+ # --------------------------------------------------------------------------
73
+ # Primary pattern: for_user()
74
+ # --------------------------------------------------------------------------
75
+
76
+ def for_user(
77
+ self,
78
+ distinct_id: str,
79
+ session_id: Optional[str] = None,
80
+ ) -> UserContext:
81
+ if distinct_id in self._user_context_cache:
82
+ return self._user_context_cache[distinct_id]
83
+
84
+ ctx = UserContext(
85
+ distinct_id=distinct_id,
86
+ site_id=self.site_id,
87
+ http_client=self._http,
88
+ session_manager=self._session_manager,
89
+ event_queue=self._event_queue,
90
+ batch_flusher=self._batch_flusher,
91
+ auto_event_manager=self._auto_event_manager,
92
+ session_id=session_id,
93
+ )
94
+ self._user_context_cache[distinct_id] = ctx
95
+ return ctx
96
+
97
+ # --------------------------------------------------------------------------
98
+ # Direct-call pattern (distinct_id as first arg)
99
+ # --------------------------------------------------------------------------
100
+
101
+ def track(
102
+ self,
103
+ distinct_id: str,
104
+ event_name: str,
105
+ properties: Optional[Dict[str, Any]] = None,
106
+ category: str = "custom",
107
+ ) -> None:
108
+ self.for_user(distinct_id).track(event_name, properties, category)
109
+
110
+ def identify(self, distinct_id: str, identify_id: str) -> IdentifyResult:
111
+ return self.for_user(distinct_id).identify(identify_id)
112
+
113
+ def wallet_address(self, distinct_id: str, wallet_addr: str) -> WalletAddressResult:
114
+ return self.for_user(distinct_id).wallet_address(wallet_addr)
115
+
116
+ def reset(self, distinct_id: str) -> ResetResult:
117
+ self._user_context_cache.pop(distinct_id, None)
118
+ return self.for_user(distinct_id).reset()
119
+
120
+ # People (direct)
121
+ def people_set(self, distinct_id: str, properties: Dict[str, Any]) -> ApiResult:
122
+ return self.for_user(distinct_id).people.set(properties)
123
+
124
+ def people_set_once(self, distinct_id: str, properties: Dict[str, Any]) -> ApiResult:
125
+ return self.for_user(distinct_id).people.set_once(properties)
126
+
127
+ def people_unset(self, distinct_id: str, keys: Union[str, List[str]]) -> ApiResult:
128
+ return self.for_user(distinct_id).people.unset(keys)
129
+
130
+ def people_increment(self, distinct_id: str, key: str, amount: float = 1) -> ApiResult:
131
+ return self.for_user(distinct_id).people.increment(key, amount)
132
+
133
+ def people_append(self, distinct_id: str, key: str, values: List[Any]) -> ApiResult:
134
+ return self.for_user(distinct_id).people.append(key, values)
135
+
136
+ def people_union(self, distinct_id: str, key: str, values: List[Any]) -> ApiResult:
137
+ return self.for_user(distinct_id).people.union(key, values)
138
+
139
+ def people_remove(self, distinct_id: str, key: str, values: List[Any]) -> ApiResult:
140
+ return self.for_user(distinct_id).people.remove(key, values)
141
+
142
+ def people_track_charge(
143
+ self,
144
+ distinct_id: str,
145
+ amount: float,
146
+ properties: Optional[Dict[str, Any]] = None,
147
+ ) -> ApiResult:
148
+ return self.for_user(distinct_id).people.track_charge(amount, properties)
149
+
150
+ def people_clear_charges(self, distinct_id: str) -> ApiResult:
151
+ return self.for_user(distinct_id).people.clear_charges()
152
+
153
+ def people_delete_user(self, distinct_id: str) -> ApiResult:
154
+ return self.for_user(distinct_id).people.delete_user()
155
+
156
+ # Groups (direct)
157
+ def set_group(
158
+ self, distinct_id: str, group_key: str, group_id: Union[str, List[str]]
159
+ ) -> ApiResult:
160
+ return self.for_user(distinct_id).set_group(group_key, group_id)
161
+
162
+ def add_group(self, distinct_id: str, group_key: str, group_id: str) -> ApiResult:
163
+ return self.for_user(distinct_id).add_group(group_key, group_id)
164
+
165
+ def remove_group(self, distinct_id: str, group_key: str, group_id: str) -> ApiResult:
166
+ return self.for_user(distinct_id).remove_group(group_key, group_id)
167
+
168
+ def get_group(
169
+ self, distinct_id: str, group_key: str, group_id: str
170
+ ) -> GroupProfile:
171
+ return self.for_user(distinct_id).get_group(group_key, group_id)
172
+
173
+ # --------------------------------------------------------------------------
174
+ # Auto events
175
+ # --------------------------------------------------------------------------
176
+
177
+ def enable_auto_events(self) -> None:
178
+ self._auto_event_manager.enable()
179
+
180
+ def disable_auto_events(self) -> None:
181
+ self._auto_event_manager.disable()
182
+
183
+ # --------------------------------------------------------------------------
184
+ # Lifecycle
185
+ # --------------------------------------------------------------------------
186
+
187
+ def flush(self) -> None:
188
+ if self._batch_flusher:
189
+ self._batch_flusher.flush()
190
+
191
+ def shutdown(self) -> None:
192
+ self._auto_event_manager.disable()
193
+ if self._batch_flusher:
194
+ self._batch_flusher.stop()
195
+ self._batch_flusher.flush()
File without changes