umami-analytics 0.1.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.
File without changes
umami/__init__.py ADDED
@@ -0,0 +1,210 @@
1
+ """umami - Umami Analytics Client for Python"""
2
+
3
+ __version__ = '0.1.0'
4
+ __author__ = 'Michael Kennedy <michael@talkpython.fm>'
5
+ __all__ = []
6
+
7
+ import sys
8
+ from typing import Optional
9
+
10
+ import httpx
11
+
12
+ from umami import models, urls # noqa: F401
13
+
14
+ url_base: Optional[str] = None
15
+ auth_token: Optional[str] = None
16
+ event_user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0'
17
+ user_agent = (f'Umami-Client v{__version__} / '
18
+ f'Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')
19
+
20
+
21
+ def set_url_base(url: str):
22
+ if not url or not url.strip():
23
+ raise Exception("URL must not be empty")
24
+
25
+ global url_base
26
+ url_base = url.strip()
27
+
28
+
29
+ async def login_async(username: str, password: str) -> models.LoginResponse:
30
+ global auth_token
31
+ validate_state(url=True)
32
+ validate_login(username, password)
33
+
34
+ url = f'{url_base}{urls.login}'
35
+ headers = {'User-Agent': user_agent}
36
+ api_data = {
37
+ "username": username,
38
+ "password": password,
39
+ }
40
+ async with httpx.AsyncClient() as client:
41
+ resp = await client.post(url, data=api_data, headers=headers)
42
+ resp.raise_for_status()
43
+
44
+ model = models.LoginResponse(**resp.json())
45
+ auth_token = model.token
46
+ return model
47
+
48
+
49
+ def login(username: str, password: str) -> models.LoginResponse:
50
+ global auth_token
51
+
52
+ validate_state(url=True)
53
+ validate_login(username, password)
54
+
55
+ url = f'{url_base}{urls.login}'
56
+ headers = {'User-Agent': user_agent}
57
+ api_data = {
58
+ "username": username,
59
+ "password": password,
60
+ }
61
+ resp = httpx.post(url, data=api_data, headers=headers)
62
+ resp.raise_for_status()
63
+
64
+ model = models.LoginResponse(**resp.json())
65
+ auth_token = model.token
66
+ return model
67
+
68
+
69
+ async def websites_async() -> list[models.Website]:
70
+ global auth_token
71
+ validate_state(url=True, user=True)
72
+
73
+ url = f'{url_base}{urls.websites}'
74
+ headers = {
75
+ 'User-Agent': user_agent,
76
+ 'Authorization': f'Bearer {auth_token}',
77
+ }
78
+ async with httpx.AsyncClient() as client:
79
+ resp = await client.get(url, headers=headers)
80
+ resp.raise_for_status()
81
+
82
+ model = models.WebsitesResponse(**resp.json())
83
+ return model.websites
84
+
85
+
86
+ def websites() -> list[models.Website]:
87
+ global auth_token
88
+ validate_state(url=True, user=True)
89
+
90
+ url = f'{url_base}{urls.websites}'
91
+ headers = {
92
+ 'User-Agent': user_agent,
93
+ 'Authorization': f'Bearer {auth_token}',
94
+ }
95
+ resp = httpx.get(url, headers=headers)
96
+ resp.raise_for_status()
97
+
98
+ data = resp.json()
99
+ model = models.WebsitesResponse(**data)
100
+ return model.websites
101
+
102
+
103
+ async def new_event_async(website_id: str, event_name: str, title: str, hostname: str, url: str = '/',
104
+ custom_data=None, referrer: Optional[str] = None, language: str = 'en-US',
105
+ screen: str = "1920x1080", ) -> str:
106
+ custom_data = custom_data or {}
107
+
108
+ payload = models.EventPayload(
109
+ website=website_id, name=event_name, title=title, hostname=hostname, url=url,
110
+ screen=screen, referrer=referrer, language=language, data=custom_data)
111
+
112
+ url = f'{url_base}{urls.events}'
113
+ headers = {
114
+ 'User-Agent': event_user_agent,
115
+ 'Authorization': f'Bearer {auth_token}',
116
+ }
117
+ event_data = {
118
+ 'payload': payload,
119
+ 'type': 'event'
120
+ }
121
+
122
+ async with httpx.AsyncClient() as client:
123
+ resp = await client.post(url, data=event_data, headers=headers)
124
+ resp.raise_for_status()
125
+
126
+ return resp.text
127
+
128
+
129
+ async def new_event_async(website_id: str, event_name: str, title: str, hostname: str, url: str = '/',
130
+ custom_data=None, referrer: Optional[str] = None, language: str = 'en-US',
131
+ screen: str = "1920x1080") -> str:
132
+ custom_data = custom_data or {}
133
+
134
+ url = f'{url_base}{urls.events}'
135
+ headers = {
136
+ 'User-Agent': event_user_agent,
137
+ 'Authorization': f'Bearer {auth_token}',
138
+ }
139
+
140
+ payload = {
141
+ "hostname": hostname,
142
+ "language": language,
143
+ "referrer": referrer,
144
+ "screen": screen,
145
+ "title": title,
146
+ "url": url,
147
+ "website": website_id,
148
+ "name": event_name,
149
+ "data": custom_data
150
+ }
151
+
152
+ event_data = {
153
+ 'payload': payload,
154
+ 'type': 'event'
155
+ }
156
+
157
+ async with httpx.AsyncClient() as client:
158
+ resp = await client.post(url, json=event_data, headers=headers)
159
+ resp.raise_for_status()
160
+
161
+ return resp.text
162
+
163
+
164
+ def new_event(website_id: str, event_name: str, title: str, hostname: str, url: str = '/',
165
+ custom_data=None, referrer: Optional[str] = None, language: str = 'en-US',
166
+ screen: str = "1920x1080") -> str:
167
+ custom_data = custom_data or {}
168
+
169
+ url = f'{url_base}{urls.events}'
170
+ headers = {
171
+ 'User-Agent': event_user_agent,
172
+ 'Authorization': f'Bearer {auth_token}',
173
+ }
174
+
175
+ payload = {
176
+ "hostname": hostname,
177
+ "language": language,
178
+ "referrer": referrer,
179
+ "screen": screen,
180
+ "title": title,
181
+ "url": url,
182
+ "website": website_id,
183
+ "name": event_name,
184
+ "data": custom_data
185
+ }
186
+
187
+ event_data = {
188
+ 'payload': payload,
189
+ 'type': 'event'
190
+ }
191
+
192
+ resp = httpx.post(url, json=event_data, headers=headers)
193
+ resp.raise_for_status()
194
+
195
+ return resp.text
196
+
197
+
198
+ def validate_login(email, password):
199
+ if not email:
200
+ raise Exception("Email cannot be empty")
201
+ if not password:
202
+ raise Exception("Password cannot be empty")
203
+
204
+
205
+ def validate_state(url=False, user=False):
206
+ if url and not url_base:
207
+ raise Exception("URL Base must be set to proceed.")
208
+
209
+ if user and not auth_token:
210
+ raise Exception("You must login before proceeding.")
@@ -0,0 +1,63 @@
1
+ import typing
2
+
3
+ import pydantic
4
+
5
+
6
+ class User(pydantic.BaseModel):
7
+ id: str
8
+ username: str
9
+ role: str
10
+ createdAt: str
11
+ isAdmin: bool
12
+
13
+
14
+ class LoginResponse(pydantic.BaseModel):
15
+ token: str
16
+ user: User
17
+
18
+
19
+ class TokenVerification(pydantic.BaseModel):
20
+ id: str
21
+ username: str
22
+ role: str
23
+ createdAt: str
24
+ isAdmin: bool
25
+
26
+
27
+ class WebsiteTeam(pydantic.BaseModel):
28
+ name: str
29
+
30
+
31
+ class WebsiteUser(pydantic.BaseModel):
32
+ username: str
33
+ id: str
34
+
35
+
36
+ class TeamSiteDetails(pydantic.BaseModel):
37
+ id: str
38
+ teamId: str
39
+ websiteId: str
40
+ createdAt: str
41
+ team: WebsiteTeam
42
+
43
+
44
+ class Website(pydantic.BaseModel):
45
+ id: str
46
+ name: typing.Optional[str] = None
47
+ domain: str
48
+ shareId: typing.Any
49
+ resetAt: typing.Any
50
+ userId: str
51
+ createdAt: str
52
+ updatedAt: str
53
+ deletedAt: typing.Any
54
+ teamWebsite: list[TeamSiteDetails]
55
+ user: WebsiteUser
56
+
57
+
58
+ class WebsitesResponse(pydantic.BaseModel):
59
+ websites: list[Website] = pydantic.Field(alias="data")
60
+ count: int
61
+ page: int
62
+ pageSize: int
63
+ orderBy: typing.Optional[str] = None
umami/urls.py ADDED
@@ -0,0 +1,3 @@
1
+ login = '/api/auth/login'
2
+ websites = '/api/websites'
3
+ events = '/api/send'
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.1
2
+ Name: umami-analytics
3
+ Version: 0.1.0
4
+ Summary: Umami Analytics Client for Python
5
+ Project-URL: Homepage, https://github.com/mikeckennedy/umami-python
6
+ Project-URL: Tracker, https://github.com/mikeckennedy/umami-python/issues
7
+ Project-URL: Source, https://github.com/mikeckennedy/umami-python
8
+ Author-email: Michael Kennedy <michael@talkpython.fm>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: analtyics,umami,website
12
+ Classifier: Development Status :: 2 - Pre-Alpha
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Requires-Python: >=3.8
22
+ Requires-Dist: httpx
23
+ Requires-Dist: pydantic
24
+ Description-Content-Type: text/markdown
25
+
26
+ ### Umami Analytics Client for Python
27
+
28
+ ## Usage
29
+
30
+ ## Installation
31
+
32
+ ### Requirements
33
+
34
+
35
+ ## Compatibility
36
+
37
+ ## Licence
38
+
39
+ ## Authors
40
+
41
+ `umami` was written by **Michael Kennedy** [michael@talkpython.fm](mailto:michael@talkpython.fm).
@@ -0,0 +1,8 @@
1
+ umami/__init__.py,sha256=7F2n8roh4C7_4lsgytanrG0WwQnHhx67QWzXanQo3g4,5840
2
+ umami/urls.py,sha256=6TICLKa0gRuYIyNNGqzAzPeq-KNKtBP5Hi5YNl1MhaM,74
3
+ umami/__impl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ umami/models/__init__.py,sha256=jc4WU84fxgxVJpB-NpZtDLeqqrgWXDRpO7-Dgy4LWZw,1091
5
+ umami_analytics-0.1.0.dist-info/METADATA,sha256=ASRXdVbwwmf_pxyVCGzWDpqmechVjXUTnDHlWFyMVlw,1221
6
+ umami_analytics-0.1.0.dist-info/WHEEL,sha256=mRYSEL3Ih6g5a_CVMIcwiF__0Ae4_gLYh01YFNwiq1k,87
7
+ umami_analytics-0.1.0.dist-info/licenses/LICENSE,sha256=12NN0UxiyDHVbhIG43LgZQLLxijQUO0Uj8TcT0RkR2A,1081
8
+ umami_analytics-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.21.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Michael Kennedy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.