azul-client 9.0.24__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.
- azul_client/__init__.py +4 -0
- azul_client/api/__init__.py +74 -0
- azul_client/api/base_api.py +163 -0
- azul_client/api/binaries_data.py +513 -0
- azul_client/api/binaries_meta.py +510 -0
- azul_client/api/features.py +175 -0
- azul_client/api/plugins.py +49 -0
- azul_client/api/purge.py +71 -0
- azul_client/api/security.py +29 -0
- azul_client/api/sources.py +51 -0
- azul_client/api/statistics.py +23 -0
- azul_client/api/users.py +29 -0
- azul_client/client.py +510 -0
- azul_client/config.py +116 -0
- azul_client/exceptions.py +30 -0
- azul_client/oidc/__init__.py +5 -0
- azul_client/oidc/callback.py +73 -0
- azul_client/oidc/oidc.py +215 -0
- azul_client-9.0.24.dist-info/METADATA +102 -0
- azul_client-9.0.24.dist-info/RECORD +23 -0
- azul_client-9.0.24.dist-info/WHEEL +5 -0
- azul_client-9.0.24.dist-info/entry_points.txt +2 -0
- azul_client-9.0.24.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""A simple server that obtains the code generated by Keycloak.
|
|
2
|
+
|
|
3
|
+
A url is presented to the user, they navigate to it in a browser and proceed with authentication.
|
|
4
|
+
Then keycloak will redirect the browser to this web server and we extract the code generated by keycloak.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import urllib.parse
|
|
9
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OIDCResponseServer(BaseHTTPRequestHandler):
|
|
13
|
+
"""Server to receive oidc callback to enable code exchange with PKCE."""
|
|
14
|
+
|
|
15
|
+
expected_path = ""
|
|
16
|
+
expected_state = ""
|
|
17
|
+
token_code = None
|
|
18
|
+
|
|
19
|
+
def do_GET(self):
|
|
20
|
+
"""Handle get request from users browser."""
|
|
21
|
+
# expected like http://127.0.0.1:8080/authorisation-code/callback?code=XXXX&state=YYYY
|
|
22
|
+
try:
|
|
23
|
+
if not self.path.startswith(f"{OIDCResponseServer.expected_path}?"):
|
|
24
|
+
raise Exception(f"bad path '{self.path}' not start with '{self.expected_path}?'")
|
|
25
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
26
|
+
qs = urllib.parse.parse_qs(parsed.query)
|
|
27
|
+
if OIDCResponseServer.expected_state != qs["state"][0]:
|
|
28
|
+
raise Exception("Returned state does not match")
|
|
29
|
+
OIDCResponseServer.token_code = qs["code"][0]
|
|
30
|
+
|
|
31
|
+
except Exception as e:
|
|
32
|
+
self.send_response(400)
|
|
33
|
+
self.send_header("Content-type", "text/html")
|
|
34
|
+
self.end_headers()
|
|
35
|
+
self.wfile.write(bytes("<html><head><title>Azul Client Auth</title></head>", "utf-8"))
|
|
36
|
+
self.wfile.write(bytes("<body>", "utf-8"))
|
|
37
|
+
self.wfile.write(bytes("<p>Failure</p>", "utf-8"))
|
|
38
|
+
self.wfile.write(bytes(f"<p>{str(e)}</p>", "utf-8"))
|
|
39
|
+
self.wfile.write(bytes("</body></html>", "utf-8"))
|
|
40
|
+
|
|
41
|
+
else:
|
|
42
|
+
self.send_response(200)
|
|
43
|
+
self.send_header("Content-type", "text/html")
|
|
44
|
+
self.end_headers()
|
|
45
|
+
self.wfile.write(bytes("<html><head><title>Azul Client Auth</title></head>", "utf-8"))
|
|
46
|
+
self.wfile.write(bytes("<body>", "utf-8"))
|
|
47
|
+
self.wfile.write(bytes("<p>Success</p>", "utf-8"))
|
|
48
|
+
self.wfile.write(bytes("<p>You may now close this tab.</p>", "utf-8"))
|
|
49
|
+
self.wfile.write(bytes("</body></html>", "utf-8"))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def receive_code(
|
|
53
|
+
expected_state: str, *, path: str = "/authorisation-code/callback", hostname: str = "localhost", port: int = 8080
|
|
54
|
+
):
|
|
55
|
+
"""Receive one http request from users browser, parse code and shut down."""
|
|
56
|
+
OIDCResponseServer.expected_path = path
|
|
57
|
+
OIDCResponseServer.expected_state = expected_state
|
|
58
|
+
server = HTTPServer((hostname, port), OIDCResponseServer)
|
|
59
|
+
print("Server started at http://%s:%s. Waiting for oauth callback." % (hostname, port), file=sys.stderr)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
while not OIDCResponseServer.token_code:
|
|
63
|
+
server.handle_request()
|
|
64
|
+
except KeyboardInterrupt:
|
|
65
|
+
pass
|
|
66
|
+
server.server_close()
|
|
67
|
+
print("Server stopped.")
|
|
68
|
+
return OIDCResponseServer.token_code
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
code = receive_code("51998", path="/test")
|
|
73
|
+
print(f"{code=}")
|
azul_client/oidc/oidc.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Auth handling."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import random
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
import urllib.parse
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from azul_client import config
|
|
16
|
+
|
|
17
|
+
from . import callback
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_json(resp: httpx.Response, dbg: str) -> dict:
|
|
21
|
+
"""Try to read json data out of the web request."""
|
|
22
|
+
if resp.status_code != 200:
|
|
23
|
+
raise Exception(f"Failed to fetch {dbg} - Code: {resp.status_code} - Content:\n{resp.content[:1000]}")
|
|
24
|
+
if "application/json" not in resp.headers.get("content-type"):
|
|
25
|
+
raise Exception(
|
|
26
|
+
f"Failed to fetch {dbg} - response mime is not json ({resp.headers.get('content-type')}). "
|
|
27
|
+
f"Is '{resp.url}' the correct endpoint? - "
|
|
28
|
+
f"Content:\n{resp.content[:1000]}"
|
|
29
|
+
)
|
|
30
|
+
try:
|
|
31
|
+
data = resp.json()
|
|
32
|
+
except json.decoder.JSONDecodeError:
|
|
33
|
+
raise Exception(
|
|
34
|
+
f"Failed to fetch {dbg} - response was not json. "
|
|
35
|
+
f"Is '{resp.url}' the correct endpoint? - "
|
|
36
|
+
f"Content:\n{resp.content[:1000]}"
|
|
37
|
+
)
|
|
38
|
+
return data
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class OIDC:
|
|
42
|
+
"""Handle authentication with Azul api."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, cfg: config.Config) -> None:
|
|
45
|
+
self.cfg = cfg
|
|
46
|
+
self._oidc_info = None
|
|
47
|
+
# Requests used to force retries on status codes [500, 502, 503, 504]
|
|
48
|
+
verify = True
|
|
49
|
+
if not self.cfg.azul_verify_ssl:
|
|
50
|
+
print("NO VERIFY SSL", file=sys.stderr)
|
|
51
|
+
verify = False
|
|
52
|
+
# Setup client with retries.
|
|
53
|
+
self._verify = verify
|
|
54
|
+
self._local_client = httpx.Client(
|
|
55
|
+
timeout=httpx.Timeout(timeout=self.cfg.max_timeout),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def _get_oidc_info(self):
|
|
59
|
+
"""Return oidc json document containing locations of required resources for auth."""
|
|
60
|
+
if not self._oidc_info:
|
|
61
|
+
self._fetch_well_known()
|
|
62
|
+
return self._oidc_info
|
|
63
|
+
|
|
64
|
+
def _get_authorization_endpoint(self):
|
|
65
|
+
return self._get_oidc_info()["authorization_endpoint"]
|
|
66
|
+
|
|
67
|
+
def _get_token_endpoint(self):
|
|
68
|
+
return self._get_oidc_info()["token_endpoint"]
|
|
69
|
+
|
|
70
|
+
def _fetch_well_known(self):
|
|
71
|
+
resp = self._local_client.get(self.cfg.oidc_url, timeout=httpx.Timeout(timeout=self.cfg.oidc_timeout))
|
|
72
|
+
self._oidc_info = _get_json(resp, "well known")
|
|
73
|
+
|
|
74
|
+
@config._lock_azul_config
|
|
75
|
+
def get_client(self):
|
|
76
|
+
"""Return a httpx client object with an up to date authorization token."""
|
|
77
|
+
out_client = httpx.Client(
|
|
78
|
+
headers={"authorization": "Bearer " + self._get_access_token()},
|
|
79
|
+
mounts={
|
|
80
|
+
"http://": httpx.HTTPTransport(retries=5),
|
|
81
|
+
"https://": httpx.HTTPTransport(retries=5),
|
|
82
|
+
},
|
|
83
|
+
timeout=httpx.Timeout(timeout=self.cfg.max_timeout),
|
|
84
|
+
verify=self._verify,
|
|
85
|
+
)
|
|
86
|
+
return out_client
|
|
87
|
+
|
|
88
|
+
def _via_service_token(self):
|
|
89
|
+
"""Retrieve a token from OIDC provider using service account flow."""
|
|
90
|
+
resp = self._local_client.post(
|
|
91
|
+
self._get_token_endpoint(),
|
|
92
|
+
data={
|
|
93
|
+
"response_type": "token",
|
|
94
|
+
"client_id": self.cfg.auth_client_id,
|
|
95
|
+
"client_secret": os.environ.get("AZUL_CLIENT_SECRET") or self.cfg.auth_client_secret,
|
|
96
|
+
"grant_type": "client_credentials",
|
|
97
|
+
"scope": self.cfg.auth_scopes,
|
|
98
|
+
},
|
|
99
|
+
timeout=httpx.Timeout(timeout=self.cfg.oidc_timeout),
|
|
100
|
+
)
|
|
101
|
+
return _get_json(resp, "via service token")
|
|
102
|
+
|
|
103
|
+
def _via_code_callback(self):
|
|
104
|
+
"""Retrieve a token from OIDC provider using code callback."""
|
|
105
|
+
# prove that requestor of token is same as receiver with code challenge and verify
|
|
106
|
+
code_verifier = base64.urlsafe_b64encode(os.urandom(40)).decode("utf-8")
|
|
107
|
+
code_verifier = re.sub("[^a-zA-Z0-9]+", "", code_verifier)
|
|
108
|
+
|
|
109
|
+
code_challenge = hashlib.sha256(code_verifier.encode("utf-8")).digest()
|
|
110
|
+
code_challenge = base64.urlsafe_b64encode(code_challenge).decode("utf-8")
|
|
111
|
+
code_challenge = code_challenge.replace("=", "")
|
|
112
|
+
|
|
113
|
+
port = 8080
|
|
114
|
+
callback_url = f"http://localhost:{port}/client/callback"
|
|
115
|
+
|
|
116
|
+
state = str(random.randint(1000000, 9999999)) # nosec B311
|
|
117
|
+
params = {
|
|
118
|
+
"response_type": "code",
|
|
119
|
+
"client_id": self.cfg.auth_client_id,
|
|
120
|
+
"redirect_uri": callback_url,
|
|
121
|
+
"state": state,
|
|
122
|
+
"scope": self.cfg.auth_scopes,
|
|
123
|
+
"code_challenge": code_challenge,
|
|
124
|
+
"code_challenge_method": "S256",
|
|
125
|
+
}
|
|
126
|
+
url_ask_user = self._get_authorization_endpoint() + "?" + urllib.parse.urlencode(params)
|
|
127
|
+
print(f"Please navigate to the following url to continue authentication:\n{url_ask_user}", file=sys.stderr)
|
|
128
|
+
code = callback.receive_code(state, path="/client/callback", hostname="localhost", port=port)
|
|
129
|
+
if not code:
|
|
130
|
+
raise Exception("No token retrieval code was returned")
|
|
131
|
+
|
|
132
|
+
resp = self._local_client.post(
|
|
133
|
+
self._get_token_endpoint(),
|
|
134
|
+
data={
|
|
135
|
+
"grant_type": "authorization_code",
|
|
136
|
+
"code": code,
|
|
137
|
+
"client_id": self.cfg.auth_client_id,
|
|
138
|
+
"state": state,
|
|
139
|
+
"scope": self.cfg.auth_scopes,
|
|
140
|
+
"redirect_uri": callback_url,
|
|
141
|
+
"code_verifier": code_verifier,
|
|
142
|
+
},
|
|
143
|
+
timeout=httpx.Timeout(timeout=self.cfg.oidc_timeout),
|
|
144
|
+
)
|
|
145
|
+
return _get_json(resp, "via code callback")
|
|
146
|
+
|
|
147
|
+
def _via_refresh(self, tk: dict):
|
|
148
|
+
"""Obtain new tokens using previously issued refresh token."""
|
|
149
|
+
refresh_token = tk.get("refresh_token", None)
|
|
150
|
+
# Full re-auth if there is no refresh token
|
|
151
|
+
if not refresh_token:
|
|
152
|
+
return self._get_token_non_refresh()
|
|
153
|
+
# need to refresh the current token
|
|
154
|
+
resp = self._local_client.post(
|
|
155
|
+
self._get_token_endpoint(),
|
|
156
|
+
data={
|
|
157
|
+
"grant_type": "refresh_token",
|
|
158
|
+
"client_id": self.cfg.auth_client_id,
|
|
159
|
+
"refresh_token": refresh_token,
|
|
160
|
+
"scope": self.cfg.auth_scopes,
|
|
161
|
+
},
|
|
162
|
+
timeout=httpx.Timeout(timeout=self.cfg.oidc_timeout),
|
|
163
|
+
)
|
|
164
|
+
if 400 <= resp.status_code < 500:
|
|
165
|
+
# maybe refresh token has expired, retry full auth
|
|
166
|
+
return None
|
|
167
|
+
return _get_json(resp, "via refresh")
|
|
168
|
+
|
|
169
|
+
def _get_token_non_refresh(self):
|
|
170
|
+
# use auth method nominated in config
|
|
171
|
+
atype = self.cfg.auth_type
|
|
172
|
+
if atype == "none":
|
|
173
|
+
# no access token is required
|
|
174
|
+
tk = {}
|
|
175
|
+
elif atype == "callback":
|
|
176
|
+
tk = self._via_code_callback()
|
|
177
|
+
elif atype == "service":
|
|
178
|
+
tk = self._via_service_token()
|
|
179
|
+
else:
|
|
180
|
+
raise NotImplementedError(atype)
|
|
181
|
+
return tk
|
|
182
|
+
|
|
183
|
+
def _get_token(self):
|
|
184
|
+
"""Get auth tokens to Azul."""
|
|
185
|
+
if self.cfg.auth_type == "none":
|
|
186
|
+
# no access token is required
|
|
187
|
+
return {}
|
|
188
|
+
|
|
189
|
+
if self.cfg.auth_token:
|
|
190
|
+
if time.time() > self.cfg.auth_token_time + 60:
|
|
191
|
+
# refresh the token
|
|
192
|
+
tk = self._via_refresh(self.cfg.auth_token)
|
|
193
|
+
if not tk:
|
|
194
|
+
# maybe refresh token has expired
|
|
195
|
+
print("Warning - Refresh token likely has expired.", file=sys.stderr)
|
|
196
|
+
tk = self._get_token_non_refresh()
|
|
197
|
+
|
|
198
|
+
elif time.time() < self.cfg.auth_token_time + 60:
|
|
199
|
+
# return current token
|
|
200
|
+
return self.cfg.auth_token
|
|
201
|
+
else:
|
|
202
|
+
tk = self._get_token_non_refresh()
|
|
203
|
+
|
|
204
|
+
self.cfg.auth_token = tk
|
|
205
|
+
self.cfg.auth_token_time = int(time.time())
|
|
206
|
+
# save updated token to config file
|
|
207
|
+
self.cfg.save()
|
|
208
|
+
return tk
|
|
209
|
+
|
|
210
|
+
def _get_access_token(self) -> str:
|
|
211
|
+
"""Get valid access token to authenticate with Azul."""
|
|
212
|
+
token = self._get_token().get("access_token", "none")
|
|
213
|
+
if not token and self.cfg.auth_type != "none":
|
|
214
|
+
raise Exception("Token required but was not found")
|
|
215
|
+
return token
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: azul-client
|
|
3
|
+
Version: 9.0.24
|
|
4
|
+
Summary: Interact with Azul using your terminal instead of clicking in the UI a thousand times!
|
|
5
|
+
Home-page: https://www.asd.gov.au/
|
|
6
|
+
Author: Azul
|
|
7
|
+
Author-email: azul@asd.gov.au
|
|
8
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: cart
|
|
20
|
+
Requires-Dist: malpz
|
|
21
|
+
Requires-Dist: click
|
|
22
|
+
Requires-Dist: pendulum
|
|
23
|
+
Requires-Dist: rich
|
|
24
|
+
Requires-Dist: httpx
|
|
25
|
+
Requires-Dist: pydantic>2
|
|
26
|
+
Requires-Dist: pydantic-settings
|
|
27
|
+
Requires-Dist: tenacity
|
|
28
|
+
Requires-Dist: azul-bedrock
|
|
29
|
+
Requires-Dist: filelock
|
|
30
|
+
Dynamic: author
|
|
31
|
+
Dynamic: author-email
|
|
32
|
+
Dynamic: classifier
|
|
33
|
+
Dynamic: description
|
|
34
|
+
Dynamic: description-content-type
|
|
35
|
+
Dynamic: home-page
|
|
36
|
+
Dynamic: requires-dist
|
|
37
|
+
Dynamic: requires-python
|
|
38
|
+
Dynamic: summary
|
|
39
|
+
|
|
40
|
+
# Azul Client
|
|
41
|
+
|
|
42
|
+
Azul client is a near complete client for Azul's RestAPI.
|
|
43
|
+
|
|
44
|
+
Interact with Azul using your terminal instead of clicking in the UI a thousand times!
|
|
45
|
+
|
|
46
|
+
Tested on ubuntu 22.04.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
`pip install azul-client`
|
|
51
|
+
|
|
52
|
+
## Setup
|
|
53
|
+
|
|
54
|
+
Azul Client requires a config file located at ~/.azul.ini
|
|
55
|
+
|
|
56
|
+
A default config will be generated on first run.
|
|
57
|
+
|
|
58
|
+
You will need to adjust the config options as appropriate.
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
[default]
|
|
62
|
+
azul_url = http://localhost
|
|
63
|
+
oidc_url = http://keycloak/.well-known/openid-configuration
|
|
64
|
+
auth_type = callback
|
|
65
|
+
auth_scopes =
|
|
66
|
+
auth_client_id = azul-web
|
|
67
|
+
auth_client_secret =
|
|
68
|
+
azul_verify_ssl = True
|
|
69
|
+
auth_token = {}
|
|
70
|
+
auth_token_time = 0
|
|
71
|
+
max_timeout = 300.0
|
|
72
|
+
oidc_timeout = 10.0
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Root CA
|
|
76
|
+
|
|
77
|
+
If you have extra Root CAs, you will need to make httpx aware of them or it will complain.
|
|
78
|
+
|
|
79
|
+
Ubuntu - `export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt`
|
|
80
|
+
|
|
81
|
+
Red Hat - `export SSL_CERT_FILE=/etc/ssl/certs/ca-bundle.crt`
|
|
82
|
+
|
|
83
|
+
Alternatively you can point to a certificate directory
|
|
84
|
+
|
|
85
|
+
`export SSL_CERT_DIR=/etc/ssl/certs`
|
|
86
|
+
|
|
87
|
+
This can be added to your ~/.bashrc to prevent you from having to do it for every terminal session.
|
|
88
|
+
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
For usage guidance refer to the [API](./docs/api.md) and [CLI](./docs/cli.md) documentation.
|
|
92
|
+
|
|
93
|
+
## Integration test suite
|
|
94
|
+
|
|
95
|
+
The integration test suite is in the tests/integration folder.
|
|
96
|
+
|
|
97
|
+
The `setUpModule` method in the file `tests/integration/__init__.py` creates all files in azul that need to be available for querying and uploading child/dataless.
|
|
98
|
+
It also waits for those uploaded files to be available in Azul which means during tests you can assume those files exist.
|
|
99
|
+
|
|
100
|
+
It also exports the sha256's of the files it uploaded to ensure the tests can import those sha256's for their testing.
|
|
101
|
+
|
|
102
|
+
NOTE - the first time you run the test suite particularly if you've added new files to the module it may be slow. But all subsequent runs will be much faster.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
azul_client/__init__.py,sha256=NLNKojPMcgSlD4B-56OivmC7Q02wbJHv5Vx4oXLgmrw,77
|
|
2
|
+
azul_client/client.py,sha256=yZNzcsuxoNA0T-GHTKFLXQuzP4-v8u-tMtmQ1Mc0zF0,17450
|
|
3
|
+
azul_client/config.py,sha256=p3exqEQaQmE85KvC05wCUyvgr29ybPMTey7moJG9zok,3652
|
|
4
|
+
azul_client/exceptions.py,sha256=WC8c5RmPpXaxGGEP5XnJD4q_CgFQAF3T6MsoTp9rOtU,1068
|
|
5
|
+
azul_client/api/__init__.py,sha256=KsQG1baHNvWlwvgKks7sUr3_vi_tCjbnEB-bInIKquc,2396
|
|
6
|
+
azul_client/api/base_api.py,sha256=ELIXpNZWIJvX6EOMdoWoOBSD19367mVgWDeCk8uImmo,6638
|
|
7
|
+
azul_client/api/binaries_data.py,sha256=ZhechdW_awfDFf6D2a6ut-tROkiUvCnZnQ_kcUIc3OI,20089
|
|
8
|
+
azul_client/api/binaries_meta.py,sha256=UvebsIKhXxATERJAlPsxvS1-gFT-7vVMETDIPCJGYDA,21237
|
|
9
|
+
azul_client/api/features.py,sha256=Kb_dJ4fVBufyvWjTJhpwmWXRR68-4A3vp10sDwYv-Yc,7089
|
|
10
|
+
azul_client/api/plugins.py,sha256=rk_qO70pNahROT5Pm4KawTNsbPmraUKEARgZK9gfaGI,2033
|
|
11
|
+
azul_client/api/purge.py,sha256=krOkKp-1UkEZXsAX71rnipxtYkHnVFOPHZmgV9tqPE0,2731
|
|
12
|
+
azul_client/api/security.py,sha256=zo6Vxz3HXqM_tZ7DpUH1q4RoRmCRj_mcfRdW7tLNbDw,1234
|
|
13
|
+
azul_client/api/sources.py,sha256=LMPOuQhRv5qnQufQBJLpc1i35NbhEJH7bQrO_e-F9sw,1911
|
|
14
|
+
azul_client/api/statistics.py,sha256=V9NDG5_GKXuMBxqCbdc05Ge1Nutjfdz6Ob64TfHrJsM,711
|
|
15
|
+
azul_client/api/users.py,sha256=neA8e-b7BhOXVj2ertADbXSpznrvY1Z5vv-M5tkMCBM,997
|
|
16
|
+
azul_client/oidc/__init__.py,sha256=6QPY9BzSjW_E8FB5LVuNvJuv2DxcGWaPSQ0UyG7ezHM,50
|
|
17
|
+
azul_client/oidc/callback.py,sha256=PRvaYoiYTxT5T1rP39xeAomGII1BrgGeKnjCx_CGZ2Y,3053
|
|
18
|
+
azul_client/oidc/oidc.py,sha256=_Ni9KGLcEkUWHYPYs0WyO8vop0lRL9IdXbutF01irug,8066
|
|
19
|
+
azul_client-9.0.24.dist-info/METADATA,sha256=KsDNcwjr9cG9Mlb6gaLxjHhTh8GdQ5NGyGArVrpD8Qc,3093
|
|
20
|
+
azul_client-9.0.24.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
21
|
+
azul_client-9.0.24.dist-info/entry_points.txt,sha256=FhUw3GodsTs_lRVuXubJQ-kaYkYA_yX_jYkKfuPQ7jE,48
|
|
22
|
+
azul_client-9.0.24.dist-info/top_level.txt,sha256=C1NP4kqciZywY_uBh4f4CDF-KqYKXe3bYI5ISHwJl7o,12
|
|
23
|
+
azul_client-9.0.24.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
azul_client
|