gpsoauth 2.0.0__tar.gz → 3.0.0__tar.gz
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.
- {gpsoauth-2.0.0 → gpsoauth-3.0.0}/PKG-INFO +38 -4
- {gpsoauth-2.0.0 → gpsoauth-3.0.0}/README.md +32 -0
- {gpsoauth-2.0.0 → gpsoauth-3.0.0}/gpsoauth/__init__.py +170 -49
- {gpsoauth-2.0.0 → gpsoauth-3.0.0}/pyproject.toml +4 -4
- {gpsoauth-2.0.0 → gpsoauth-3.0.0}/LICENSE +0 -0
- {gpsoauth-2.0.0 → gpsoauth-3.0.0}/gpsoauth/google.py +0 -0
- {gpsoauth-2.0.0 → gpsoauth-3.0.0}/gpsoauth/py.typed +0 -0
- {gpsoauth-2.0.0 → gpsoauth-3.0.0}/gpsoauth/util.py +0 -0
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: gpsoauth
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: A python client library for Google Play Services OAuth.
|
|
5
5
|
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Author: Simon Weber
|
|
7
8
|
Author-email: simon@simonmweber.com
|
|
8
|
-
Requires-Python: >=3.
|
|
9
|
+
Requires-Python: >=3.10,<4.0
|
|
9
10
|
Classifier: Development Status :: 5 - Production/Stable
|
|
10
11
|
Classifier: Intended Audience :: Developers
|
|
11
12
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
13
|
Classifier: Operating System :: OS Independent
|
|
13
14
|
Classifier: Programming Language :: Python
|
|
14
15
|
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
20
22
|
Classifier: Typing :: Typed
|
|
21
23
|
Requires-Dist: pycryptodomex (>=3.0)
|
|
22
24
|
Requires-Dist: requests (>=2.0.0)
|
|
@@ -89,8 +91,40 @@ auth_response = gpsoauth.perform_oauth(
|
|
|
89
91
|
token = auth_response['Auth']
|
|
90
92
|
```
|
|
91
93
|
|
|
94
|
+
## Using another HTTP client
|
|
95
|
+
|
|
96
|
+
`perform_master_login()`, `exchange_token()` and `perform_oauth()` send their requests with [requests](https://requests.readthedocs.io/). To send them with another HTTP client, such as an async one, build the form fields with `build_master_login_data()`, `build_exchange_token_data()` or `build_oauth_data()`, which take the same arguments except `proxy`. For example, with [aiohttp](https://docs.aiohttp.org/):
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from urllib.parse import urlencode
|
|
100
|
+
|
|
101
|
+
import aiohttp
|
|
102
|
+
import gpsoauth
|
|
103
|
+
|
|
104
|
+
# Loading the certificates blocks: create the context once, outside the event loop.
|
|
105
|
+
ssl_context = gpsoauth.create_ssl_context()
|
|
106
|
+
|
|
107
|
+
async def perform_oauth(email, master_token, android_id):
|
|
108
|
+
data = gpsoauth.build_oauth_data(
|
|
109
|
+
email, master_token, android_id,
|
|
110
|
+
service='sj', app='com.google.android.music',
|
|
111
|
+
client_sig='...')
|
|
112
|
+
async with aiohttp.ClientSession() as session, session.post(
|
|
113
|
+
gpsoauth.AUTH_URL,
|
|
114
|
+
data=urlencode(data).encode('ascii'),
|
|
115
|
+
headers=gpsoauth.build_auth_headers(),
|
|
116
|
+
ssl=ssl_context,
|
|
117
|
+
) as response:
|
|
118
|
+
return gpsoauth.google.parse_auth_response(await response.text())
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
These functions give you the form fields, the headers gpsoauth sets and its TLS settings. Your client still adds its own default headers, and handles proxies, timeouts and HTTP errors as it does for its other requests. It must also URL-encode the fields, as in the example: `build_master_login_data()` returns `EncryptedPasswd` as bytes, which aiohttp would otherwise send as a file upload.
|
|
122
|
+
|
|
123
|
+
`create_ssl_context()` checks the server's certificate and hostname, and trusts the system's certificate authorities. gpsoauth's own functions may also trust the CA bundle requests uses, depending on its version and settings: certifi's, or the file named by `REQUESTS_CA_BUNDLE` or `CURL_CA_BUNDLE`. So a certificate that gpsoauth's functions accept can fail with another client. To trust the same certificates, add that bundle with `ssl_context.load_verify_locations()`, for example `ssl_context.load_verify_locations(requests.certs.where())` for certifi's. This also blocks, so do it where you create the context, outside the event loop.
|
|
124
|
+
|
|
92
125
|
## Ports
|
|
93
126
|
|
|
127
|
+
- Typescript: <https://github.com/billgreenwald/gpsoauth-js>
|
|
94
128
|
- C\#: <https://github.com/vemacs/GPSOAuthSharp>
|
|
95
129
|
- Ruby: <https://github.com/bryanmytko/gpsoauth>
|
|
96
130
|
- Java: <https://github.com/svarzee/gpsoauth-java>
|
|
@@ -62,8 +62,40 @@ auth_response = gpsoauth.perform_oauth(
|
|
|
62
62
|
token = auth_response['Auth']
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
## Using another HTTP client
|
|
66
|
+
|
|
67
|
+
`perform_master_login()`, `exchange_token()` and `perform_oauth()` send their requests with [requests](https://requests.readthedocs.io/). To send them with another HTTP client, such as an async one, build the form fields with `build_master_login_data()`, `build_exchange_token_data()` or `build_oauth_data()`, which take the same arguments except `proxy`. For example, with [aiohttp](https://docs.aiohttp.org/):
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from urllib.parse import urlencode
|
|
71
|
+
|
|
72
|
+
import aiohttp
|
|
73
|
+
import gpsoauth
|
|
74
|
+
|
|
75
|
+
# Loading the certificates blocks: create the context once, outside the event loop.
|
|
76
|
+
ssl_context = gpsoauth.create_ssl_context()
|
|
77
|
+
|
|
78
|
+
async def perform_oauth(email, master_token, android_id):
|
|
79
|
+
data = gpsoauth.build_oauth_data(
|
|
80
|
+
email, master_token, android_id,
|
|
81
|
+
service='sj', app='com.google.android.music',
|
|
82
|
+
client_sig='...')
|
|
83
|
+
async with aiohttp.ClientSession() as session, session.post(
|
|
84
|
+
gpsoauth.AUTH_URL,
|
|
85
|
+
data=urlencode(data).encode('ascii'),
|
|
86
|
+
headers=gpsoauth.build_auth_headers(),
|
|
87
|
+
ssl=ssl_context,
|
|
88
|
+
) as response:
|
|
89
|
+
return gpsoauth.google.parse_auth_response(await response.text())
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
These functions give you the form fields, the headers gpsoauth sets and its TLS settings. Your client still adds its own default headers, and handles proxies, timeouts and HTTP errors as it does for its other requests. It must also URL-encode the fields, as in the example: `build_master_login_data()` returns `EncryptedPasswd` as bytes, which aiohttp would otherwise send as a file upload.
|
|
93
|
+
|
|
94
|
+
`create_ssl_context()` checks the server's certificate and hostname, and trusts the system's certificate authorities. gpsoauth's own functions may also trust the CA bundle requests uses, depending on its version and settings: certifi's, or the file named by `REQUESTS_CA_BUNDLE` or `CURL_CA_BUNDLE`. So a certificate that gpsoauth's functions accept can fail with another client. To trust the same certificates, add that bundle with `ssl_context.load_verify_locations()`, for example `ssl_context.load_verify_locations(requests.certs.where())` for certifi's. This also blocks, so do it where you create the context, outside the event loop.
|
|
95
|
+
|
|
65
96
|
## Ports
|
|
66
97
|
|
|
98
|
+
- Typescript: <https://github.com/billgreenwald/gpsoauth-js>
|
|
67
99
|
- C\#: <https://github.com/vemacs/GPSOAuthSharp>
|
|
68
100
|
- Ruby: <https://github.com/bryanmytko/gpsoauth>
|
|
69
101
|
- Java: <https://github.com/svarzee/gpsoauth-java>
|
|
@@ -62,24 +62,61 @@ class SSLContext(ssl.SSLContext):
|
|
|
62
62
|
"""
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
def create_ssl_context() -> ssl.SSLContext:
|
|
66
|
+
"""
|
|
67
|
+
Return a new SSL context with the TLS settings gpsoauth uses for AUTH_URL.
|
|
68
|
+
|
|
69
|
+
It is built as the one gpsoauth's own requests use: SSLContext() without a
|
|
70
|
+
protocol argument, with urllib3's ciphers if urllib3 < 2. It requires a
|
|
71
|
+
valid certificate for the server's hostname and trusts the system's
|
|
72
|
+
certificate authorities. Unlike urllib3's default context, it allows TLS
|
|
73
|
+
session tickets, and unlike urllib3's and aiohttp's, it offers no ALPN
|
|
74
|
+
(its set_alpn_protocols() does nothing): Google returns 403 Bad
|
|
75
|
+
Authentication if tickets are disabled or ALPN is offered.
|
|
76
|
+
|
|
77
|
+
gpsoauth's own functions may also trust the CA bundle requests uses,
|
|
78
|
+
depending on its version and settings: certifi's, or the file named by
|
|
79
|
+
REQUESTS_CA_BUNDLE or CURL_CA_BUNDLE. To trust the same certificates, add
|
|
80
|
+
that bundle with the context's load_verify_locations().
|
|
81
|
+
|
|
82
|
+
Loading certificates blocks, so in async code create the context once,
|
|
83
|
+
outside the event loop, and add any bundle there too.
|
|
84
|
+
"""
|
|
85
|
+
context = SSLContext()
|
|
86
|
+
if SSL_DEFAULT_CIPHERS:
|
|
87
|
+
context.set_ciphers(SSL_DEFAULT_CIPHERS)
|
|
88
|
+
context.verify_mode = ssl.CERT_REQUIRED
|
|
89
|
+
context.check_hostname = True
|
|
90
|
+
context.options &= ~ssl.OP_NO_TICKET # pylint: disable=E1101
|
|
91
|
+
context.load_default_certs()
|
|
92
|
+
return context
|
|
93
|
+
|
|
94
|
+
|
|
65
95
|
class AuthHTTPAdapter(requests.adapters.HTTPAdapter):
|
|
66
96
|
"""TLS tweaks."""
|
|
67
97
|
|
|
68
98
|
def init_poolmanager(self, *args: Any, **kwargs: Any) -> None:
|
|
69
|
-
"""
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
context = SSLContext()
|
|
75
|
-
if SSL_DEFAULT_CIPHERS:
|
|
76
|
-
context.set_ciphers(SSL_DEFAULT_CIPHERS)
|
|
77
|
-
context.verify_mode = ssl.CERT_REQUIRED
|
|
78
|
-
context.options &= ~ssl.OP_NO_TICKET # pylint: disable=E1101
|
|
79
|
-
context.load_default_certs()
|
|
99
|
+
"""Use the TLS settings of create_ssl_context()."""
|
|
100
|
+
context = create_ssl_context()
|
|
101
|
+
# Leave the hostname check to urllib3, as gpsoauth always has: its
|
|
102
|
+
# matching differs from the ssl module's, e.g. for partial wildcards.
|
|
103
|
+
context.check_hostname = False
|
|
80
104
|
self.poolmanager = PoolManager(*args, ssl_context=context, **kwargs)
|
|
81
105
|
|
|
82
106
|
|
|
107
|
+
def build_auth_headers() -> dict[str, str]:
|
|
108
|
+
"""
|
|
109
|
+
Return the headers gpsoauth sets on its requests to AUTH_URL.
|
|
110
|
+
|
|
111
|
+
HTTP clients add their own default headers to these, such as Accept.
|
|
112
|
+
"""
|
|
113
|
+
return {
|
|
114
|
+
"Accept-Encoding": "identity",
|
|
115
|
+
"Content-type": "application/x-www-form-urlencoded",
|
|
116
|
+
"User-Agent": USER_AGENT,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
83
120
|
def _perform_auth_request(
|
|
84
121
|
data: dict[str, int | str | bytes], proxies: MutableMapping[str, str] | None = None
|
|
85
122
|
) -> dict[str, str]:
|
|
@@ -87,19 +124,52 @@ def _perform_auth_request(
|
|
|
87
124
|
session.mount(AUTH_URL, AuthHTTPAdapter())
|
|
88
125
|
if proxies:
|
|
89
126
|
session.proxies = proxies
|
|
90
|
-
session.headers.update(
|
|
91
|
-
{
|
|
92
|
-
"Accept-Encoding": "identity",
|
|
93
|
-
"Content-type": "application/x-www-form-urlencoded",
|
|
94
|
-
"User-Agent": USER_AGENT,
|
|
95
|
-
}
|
|
96
|
-
)
|
|
127
|
+
session.headers.update(build_auth_headers())
|
|
97
128
|
|
|
98
129
|
res = session.post(AUTH_URL, data=data, verify=True)
|
|
99
130
|
|
|
100
131
|
return google.parse_auth_response(res.text)
|
|
101
132
|
|
|
102
133
|
|
|
134
|
+
def build_master_login_data(
|
|
135
|
+
email: str,
|
|
136
|
+
password: str,
|
|
137
|
+
android_id: str,
|
|
138
|
+
service: str = "ac2dm",
|
|
139
|
+
device_country: str = "us",
|
|
140
|
+
operator_country: str = "us",
|
|
141
|
+
lang: str = "en",
|
|
142
|
+
sdk_version: int = 17,
|
|
143
|
+
client_sig: str = "38918a453d07199354f8b19af05ec6562ced5788",
|
|
144
|
+
) -> dict[str, int | str | bytes]:
|
|
145
|
+
"""
|
|
146
|
+
Return the form fields perform_master_login() posts to AUTH_URL, as a new dict.
|
|
147
|
+
|
|
148
|
+
It takes the same arguments, except proxy. EncryptedPasswd differs on each
|
|
149
|
+
call, as the encryption is randomized.
|
|
150
|
+
"""
|
|
151
|
+
return {
|
|
152
|
+
"accountType": "HOSTED_OR_GOOGLE",
|
|
153
|
+
"Email": email,
|
|
154
|
+
"has_permission": 1,
|
|
155
|
+
"add_account": 1,
|
|
156
|
+
"EncryptedPasswd": google.construct_signature(
|
|
157
|
+
email, password, ANDROID_KEY_7_3_29
|
|
158
|
+
),
|
|
159
|
+
"service": service,
|
|
160
|
+
"source": "android",
|
|
161
|
+
"androidId": android_id,
|
|
162
|
+
"device_country": device_country,
|
|
163
|
+
"operatorCountry": operator_country,
|
|
164
|
+
"lang": lang,
|
|
165
|
+
"sdk_version": sdk_version,
|
|
166
|
+
"google_play_services_version": 240913000,
|
|
167
|
+
"client_sig": client_sig,
|
|
168
|
+
"callerSig": client_sig,
|
|
169
|
+
"droidguard_results": "dummy123",
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
103
173
|
def perform_master_login(
|
|
104
174
|
email: str,
|
|
105
175
|
password: str,
|
|
@@ -132,14 +202,44 @@ def perform_master_login(
|
|
|
132
202
|
}
|
|
133
203
|
"""
|
|
134
204
|
|
|
135
|
-
data
|
|
205
|
+
data = build_master_login_data(
|
|
206
|
+
email,
|
|
207
|
+
password,
|
|
208
|
+
android_id,
|
|
209
|
+
service=service,
|
|
210
|
+
device_country=device_country,
|
|
211
|
+
operator_country=operator_country,
|
|
212
|
+
lang=lang,
|
|
213
|
+
sdk_version=sdk_version,
|
|
214
|
+
client_sig=client_sig,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return _perform_auth_request(data, proxy)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def build_exchange_token_data(
|
|
221
|
+
email: str,
|
|
222
|
+
token: str,
|
|
223
|
+
android_id: str,
|
|
224
|
+
service: str = "ac2dm",
|
|
225
|
+
device_country: str = "us",
|
|
226
|
+
operator_country: str = "us",
|
|
227
|
+
lang: str = "en",
|
|
228
|
+
sdk_version: int = 17,
|
|
229
|
+
client_sig: str = "38918a453d07199354f8b19af05ec6562ced5788",
|
|
230
|
+
) -> dict[str, int | str | bytes]:
|
|
231
|
+
"""
|
|
232
|
+
Return the form fields exchange_token() posts to AUTH_URL, as a new dict.
|
|
233
|
+
|
|
234
|
+
It takes the same arguments, except proxy.
|
|
235
|
+
"""
|
|
236
|
+
return {
|
|
136
237
|
"accountType": "HOSTED_OR_GOOGLE",
|
|
137
238
|
"Email": email,
|
|
138
239
|
"has_permission": 1,
|
|
139
240
|
"add_account": 1,
|
|
140
|
-
"
|
|
141
|
-
|
|
142
|
-
),
|
|
241
|
+
"ACCESS_TOKEN": 1,
|
|
242
|
+
"Token": token,
|
|
143
243
|
"service": service,
|
|
144
244
|
"source": "android",
|
|
145
245
|
"androidId": android_id,
|
|
@@ -153,8 +253,6 @@ def perform_master_login(
|
|
|
153
253
|
"droidguard_results": "dummy123",
|
|
154
254
|
}
|
|
155
255
|
|
|
156
|
-
return _perform_auth_request(data, proxy)
|
|
157
|
-
|
|
158
256
|
|
|
159
257
|
def exchange_token(
|
|
160
258
|
email: str,
|
|
@@ -188,28 +286,55 @@ def exchange_token(
|
|
|
188
286
|
}
|
|
189
287
|
"""
|
|
190
288
|
|
|
191
|
-
data
|
|
289
|
+
data = build_exchange_token_data(
|
|
290
|
+
email,
|
|
291
|
+
token,
|
|
292
|
+
android_id,
|
|
293
|
+
service=service,
|
|
294
|
+
device_country=device_country,
|
|
295
|
+
operator_country=operator_country,
|
|
296
|
+
lang=lang,
|
|
297
|
+
sdk_version=sdk_version,
|
|
298
|
+
client_sig=client_sig,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
return _perform_auth_request(data, proxy)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def build_oauth_data(
|
|
305
|
+
email: str,
|
|
306
|
+
master_token: str,
|
|
307
|
+
android_id: str,
|
|
308
|
+
service: str,
|
|
309
|
+
app: str,
|
|
310
|
+
client_sig: str,
|
|
311
|
+
device_country: str = "us",
|
|
312
|
+
operator_country: str = "us",
|
|
313
|
+
lang: str = "en",
|
|
314
|
+
sdk_version: int = 17,
|
|
315
|
+
) -> dict[str, int | str | bytes]:
|
|
316
|
+
"""
|
|
317
|
+
Return the form fields perform_oauth() posts to AUTH_URL, as a new dict.
|
|
318
|
+
|
|
319
|
+
It takes the same arguments, except proxy.
|
|
320
|
+
"""
|
|
321
|
+
return {
|
|
192
322
|
"accountType": "HOSTED_OR_GOOGLE",
|
|
193
323
|
"Email": email,
|
|
194
324
|
"has_permission": 1,
|
|
195
|
-
"
|
|
196
|
-
"ACCESS_TOKEN": 1,
|
|
197
|
-
"Token": token,
|
|
325
|
+
"EncryptedPasswd": master_token,
|
|
198
326
|
"service": service,
|
|
199
327
|
"source": "android",
|
|
200
328
|
"androidId": android_id,
|
|
329
|
+
"app": app,
|
|
330
|
+
"client_sig": client_sig,
|
|
201
331
|
"device_country": device_country,
|
|
202
332
|
"operatorCountry": operator_country,
|
|
203
333
|
"lang": lang,
|
|
204
334
|
"sdk_version": sdk_version,
|
|
205
335
|
"google_play_services_version": 240913000,
|
|
206
|
-
"client_sig": client_sig,
|
|
207
|
-
"callerSig": client_sig,
|
|
208
|
-
"droidguard_results": "dummy123",
|
|
209
336
|
}
|
|
210
337
|
|
|
211
|
-
return _perform_auth_request(data, proxy)
|
|
212
|
-
|
|
213
338
|
|
|
214
339
|
def perform_oauth(
|
|
215
340
|
email: str,
|
|
@@ -241,21 +366,17 @@ def perform_oauth(
|
|
|
241
366
|
``Authorization: GoogleLogin auth=res['Auth']``.
|
|
242
367
|
"""
|
|
243
368
|
|
|
244
|
-
data
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
"lang": lang,
|
|
257
|
-
"sdk_version": sdk_version,
|
|
258
|
-
"google_play_services_version": 240913000,
|
|
259
|
-
}
|
|
369
|
+
data = build_oauth_data(
|
|
370
|
+
email,
|
|
371
|
+
master_token,
|
|
372
|
+
android_id,
|
|
373
|
+
service=service,
|
|
374
|
+
app=app,
|
|
375
|
+
client_sig=client_sig,
|
|
376
|
+
device_country=device_country,
|
|
377
|
+
operator_country=operator_country,
|
|
378
|
+
lang=lang,
|
|
379
|
+
sdk_version=sdk_version,
|
|
380
|
+
)
|
|
260
381
|
|
|
261
382
|
return _perform_auth_request(data, proxy)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "gpsoauth"
|
|
3
|
-
version = "
|
|
3
|
+
version = "3.0.0"
|
|
4
4
|
description = "A python client library for Google Play Services OAuth."
|
|
5
5
|
authors = ["Simon Weber <simon@simonmweber.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -11,11 +11,11 @@ classifiers = [
|
|
|
11
11
|
"Operating System :: OS Independent",
|
|
12
12
|
"Programming Language :: Python",
|
|
13
13
|
"Programming Language :: Python :: 3",
|
|
14
|
-
"Programming Language :: Python :: 3.9",
|
|
15
14
|
"Programming Language :: Python :: 3.10",
|
|
16
15
|
"Programming Language :: Python :: 3.11",
|
|
17
16
|
"Programming Language :: Python :: 3.12",
|
|
18
17
|
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Programming Language :: Python :: 3.14",
|
|
19
19
|
"Typing :: Typed",
|
|
20
20
|
]
|
|
21
21
|
readme = "README.md"
|
|
@@ -24,7 +24,7 @@ repository = "https://github.com/simon-weber/gpsoauth"
|
|
|
24
24
|
include = ["gpsoauth/py.typed"]
|
|
25
25
|
|
|
26
26
|
[tool.poetry.dependencies]
|
|
27
|
-
python = "^3.
|
|
27
|
+
python = "^3.10"
|
|
28
28
|
pycryptodomex = ">= 3.0"
|
|
29
29
|
requests = ">= 2.0.0"
|
|
30
30
|
urllib3 = ">= 1.26.0"
|
|
@@ -33,7 +33,7 @@ urllib3 = ">= 1.26.0"
|
|
|
33
33
|
pre-commit = "^2.11.1"
|
|
34
34
|
black = "^22.3.0"
|
|
35
35
|
isort = "^5.8.0"
|
|
36
|
-
pytest = "^
|
|
36
|
+
pytest = "^8.4"
|
|
37
37
|
flake8 = "^7.0"
|
|
38
38
|
flake8-bugbear = ">= 21.4.1"
|
|
39
39
|
flake8-comprehensions = ">= 3.4.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|