prefect-cloud 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.
- prefect_cloud/__init__.py +0 -0
- prefect_cloud/auth.py +419 -0
- prefect_cloud/cli/__init__.py +0 -0
- prefect_cloud/cli/completions.py +54 -0
- prefect_cloud/cli/root.py +468 -0
- prefect_cloud/cli/utilities.py +202 -0
- prefect_cloud/client.py +832 -0
- prefect_cloud/dependencies.py +80 -0
- prefect_cloud/deployments.py +88 -0
- prefect_cloud/github.py +113 -0
- prefect_cloud/py.typed +0 -0
- prefect_cloud/schemas/__init__.py +0 -0
- prefect_cloud/schemas/actions.py +88 -0
- prefect_cloud/schemas/objects.py +167 -0
- prefect_cloud/schemas/responses.py +14 -0
- prefect_cloud/settings.py +18 -0
- prefect_cloud/types.py +56 -0
- prefect_cloud/utilities/__init__.py +0 -0
- prefect_cloud/utilities/blocks.py +15 -0
- prefect_cloud/utilities/callables.py +683 -0
- prefect_cloud/utilities/exception.py +41 -0
- prefect_cloud/utilities/flows.py +55 -0
- prefect_cloud/utilities/generics.py +63 -0
- prefect_cloud/utilities/pendulum.py +78 -0
- prefect_cloud/utilities/tui.py +80 -0
- prefect_cloud-0.1.0.dist-info/METADATA +112 -0
- prefect_cloud-0.1.0.dist-info/RECORD +29 -0
- prefect_cloud-0.1.0.dist-info/WHEEL +4 -0
- prefect_cloud-0.1.0.dist-info/entry_points.txt +2 -0
|
File without changes
|
prefect_cloud/auth.py
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import socket
|
|
4
|
+
import threading
|
|
5
|
+
import webbrowser
|
|
6
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
7
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from queue import Queue
|
|
10
|
+
from typing import Any, AsyncGenerator, Generator, Sequence
|
|
11
|
+
from urllib.parse import parse_qs, quote, urlparse
|
|
12
|
+
from uuid import UUID
|
|
13
|
+
|
|
14
|
+
import toml
|
|
15
|
+
from pydantic import BaseModel, TypeAdapter
|
|
16
|
+
|
|
17
|
+
from prefect_cloud.client import PrefectCloudClient, SyncPrefectCloudClient
|
|
18
|
+
from prefect_cloud.utilities.tui import prompt_select_from_list
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _get_cloud_urls() -> tuple[str, str]:
|
|
22
|
+
"""Get the appropriate Cloud UI and API URLs based on environment"""
|
|
23
|
+
|
|
24
|
+
env = os.environ.get("CLOUD_ENV")
|
|
25
|
+
from_env = os.environ.get("PREFECT_CLOUD_API_URL")
|
|
26
|
+
|
|
27
|
+
if from_env:
|
|
28
|
+
cloud_api_url = from_env
|
|
29
|
+
elif env in ("prd", "prod", None):
|
|
30
|
+
cloud_api_url = "https://api.prefect.cloud/api"
|
|
31
|
+
elif env == "stg":
|
|
32
|
+
cloud_api_url = "https://api.stg.prefect.dev/api"
|
|
33
|
+
elif env == "dev":
|
|
34
|
+
cloud_api_url = "https://api.prefect.dev/api"
|
|
35
|
+
elif env == "lcl":
|
|
36
|
+
cloud_api_url = "http://localhost:8000/api"
|
|
37
|
+
else:
|
|
38
|
+
raise ValueError(f"Invalid CLOUD_ENV: {env}")
|
|
39
|
+
|
|
40
|
+
if os.environ.get("PREFECT_CLOUD_UI_URL"):
|
|
41
|
+
cloud_ui_url = os.environ["PREFECT_CLOUD_UI_URL"]
|
|
42
|
+
else:
|
|
43
|
+
cloud_ui_url = cloud_api_url.replace("https://api.", "https://app.").replace(
|
|
44
|
+
"/api", ""
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return cloud_ui_url, cloud_api_url
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
CLOUD_UI_URL, CLOUD_API_URL = _get_cloud_urls()
|
|
51
|
+
|
|
52
|
+
PREFECT_HOME = Path.home() / ".prefect"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Account(BaseModel):
|
|
56
|
+
account_id: UUID
|
|
57
|
+
account_name: str
|
|
58
|
+
account_handle: str
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Workspace(BaseModel):
|
|
62
|
+
account_id: UUID
|
|
63
|
+
account_name: str
|
|
64
|
+
account_handle: str
|
|
65
|
+
workspace_id: UUID
|
|
66
|
+
workspace_name: str
|
|
67
|
+
workspace_handle: str
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def full_handle(self) -> str:
|
|
71
|
+
return f"{self.account_handle}/{self.workspace_handle}"
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def api_url(self) -> str:
|
|
75
|
+
return (
|
|
76
|
+
f"{CLOUD_API_URL}/accounts/{self.account_id}/workspaces/{self.workspace_id}"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Me(BaseModel):
|
|
81
|
+
id: UUID
|
|
82
|
+
email: str
|
|
83
|
+
first_name: str
|
|
84
|
+
last_name: str
|
|
85
|
+
handle: str
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def login(api_key: str | None = None, workspace_id_or_slug: str | None = None):
|
|
89
|
+
"""Logs the user into Prefect Cloud interactively, setting their active profile"""
|
|
90
|
+
if not api_key:
|
|
91
|
+
api_key = get_api_key()
|
|
92
|
+
|
|
93
|
+
if not api_key or not await key_is_valid(api_key):
|
|
94
|
+
api_key = login_interactively()
|
|
95
|
+
if not api_key:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
workspace: Workspace | None = None
|
|
99
|
+
if workspace_id_or_slug:
|
|
100
|
+
workspace = await lookup_workspace(api_key, workspace_id_or_slug)
|
|
101
|
+
|
|
102
|
+
if not workspace:
|
|
103
|
+
workspace = await prompt_for_workspace(api_key)
|
|
104
|
+
if not workspace:
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
set_cloud_profile(api_key, workspace)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def logout():
|
|
111
|
+
"""Logs the user out of Prefect Cloud"""
|
|
112
|
+
remove_cloud_profile()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@asynccontextmanager
|
|
116
|
+
async def cloud_client(api_key: str) -> AsyncGenerator[PrefectCloudClient, None]:
|
|
117
|
+
"""Creates a client for the Prefect Cloud API"""
|
|
118
|
+
|
|
119
|
+
async with PrefectCloudClient(api_url=CLOUD_API_URL, api_key=api_key) as client:
|
|
120
|
+
yield client
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@contextmanager
|
|
124
|
+
def sync_cloud_client(api_key: str) -> Generator[SyncPrefectCloudClient, None, None]:
|
|
125
|
+
"""Creates a client for the Prefect Cloud API"""
|
|
126
|
+
with SyncPrefectCloudClient(api_url=CLOUD_API_URL, api_key=api_key) as client:
|
|
127
|
+
yield client
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def get_prefect_cloud_client() -> PrefectCloudClient:
|
|
131
|
+
_, api_url, api_key = await get_cloud_urls_or_login()
|
|
132
|
+
return PrefectCloudClient(
|
|
133
|
+
api_url=api_url,
|
|
134
|
+
api_key=api_key,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
async def get_cloud_urls_or_login() -> tuple[str, str, str]:
|
|
139
|
+
"""Gets the cloud UI URL, API URL, and API key"""
|
|
140
|
+
ui_url, api_url, api_key = get_cloud_urls_without_login()
|
|
141
|
+
if not ui_url or not api_url or not api_key:
|
|
142
|
+
await login()
|
|
143
|
+
|
|
144
|
+
ui_url, api_url, api_key = get_cloud_urls_without_login()
|
|
145
|
+
if not ui_url or not api_url or not api_key:
|
|
146
|
+
raise ValueError("No cloud profile found")
|
|
147
|
+
|
|
148
|
+
return ui_url, api_url, api_key
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_cloud_urls_without_login() -> tuple[str | None, str | None, str | None]:
|
|
152
|
+
"""Gets the cloud UI URL, API URL, and API key"""
|
|
153
|
+
api_url: str | None = get_api_url()
|
|
154
|
+
if not api_url:
|
|
155
|
+
return None, None, None
|
|
156
|
+
|
|
157
|
+
ui_url = (
|
|
158
|
+
api_url.replace("https://api.", "https://app.")
|
|
159
|
+
.replace("/api", "")
|
|
160
|
+
.replace("/accounts/", "/account/")
|
|
161
|
+
.replace("/workspaces/", "/workspace/")
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
api_key = get_api_key()
|
|
165
|
+
if not api_key:
|
|
166
|
+
return None, None, None
|
|
167
|
+
|
|
168
|
+
return ui_url, api_url, api_key
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def get_api_key_or_login() -> str:
|
|
172
|
+
"""Gets a validated API key or logs the user in if no API key is available"""
|
|
173
|
+
api_key = get_api_key()
|
|
174
|
+
if not api_key:
|
|
175
|
+
api_key = login_interactively()
|
|
176
|
+
if not api_key:
|
|
177
|
+
raise ValueError("No API key found")
|
|
178
|
+
return api_key
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
async def key_is_valid(api_key: str) -> bool:
|
|
182
|
+
"""Checks if the given API key is valid"""
|
|
183
|
+
async with cloud_client(api_key) as client:
|
|
184
|
+
response = await client.request("GET", "/me/")
|
|
185
|
+
return response.status_code == 200
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def login_interactively() -> str | None:
|
|
189
|
+
"""Logs the user into Prefect Cloud interactively"""
|
|
190
|
+
result_queue: Queue[str | None] = Queue()
|
|
191
|
+
with login_server(result_queue) as uri:
|
|
192
|
+
login_url = f"{CLOUD_UI_URL}/auth/client?callback={quote(uri)}&g=true"
|
|
193
|
+
|
|
194
|
+
threading.Thread(
|
|
195
|
+
target=webbrowser.open_new_tab, args=(login_url,), daemon=True
|
|
196
|
+
).start()
|
|
197
|
+
|
|
198
|
+
return result_queue.get()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@contextmanager
|
|
202
|
+
def login_server(result_queue: Queue[str | None]) -> Generator[str, None, None]:
|
|
203
|
+
"""Runs a local server to handle the callback from Prefect Cloud"""
|
|
204
|
+
|
|
205
|
+
class LoginHandler(BaseHTTPRequestHandler):
|
|
206
|
+
def log_message(self, format: str, *args: Any) -> None:
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
def add_cors_headers(self) -> None:
|
|
210
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
211
|
+
self.send_header("Access-Control-Allow-Headers", "*")
|
|
212
|
+
self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS")
|
|
213
|
+
|
|
214
|
+
def do_OPTIONS(self) -> None:
|
|
215
|
+
self.send_response(200)
|
|
216
|
+
self.add_cors_headers()
|
|
217
|
+
self.end_headers()
|
|
218
|
+
|
|
219
|
+
def do_GET(self) -> None:
|
|
220
|
+
query_params = parse_qs(urlparse(self.path).query)
|
|
221
|
+
api_key = query_params.get("key", [""])[0] or None
|
|
222
|
+
|
|
223
|
+
self.send_response(200)
|
|
224
|
+
self.send_header("Content-type", "text/html")
|
|
225
|
+
self.end_headers()
|
|
226
|
+
self.wfile.write(b"""
|
|
227
|
+
<script>
|
|
228
|
+
if (window.opener) {
|
|
229
|
+
window.opener.postMessage('auth-complete', '*')
|
|
230
|
+
}
|
|
231
|
+
</script>
|
|
232
|
+
<p>You can close this window.</p>
|
|
233
|
+
""")
|
|
234
|
+
result_queue.put(api_key or None)
|
|
235
|
+
|
|
236
|
+
def do_POST(self):
|
|
237
|
+
content_length = int(self.headers.get("Content-Length", 0))
|
|
238
|
+
body = self.rfile.read(content_length)
|
|
239
|
+
data = json.loads(body)
|
|
240
|
+
api_key = data.get("api_key")
|
|
241
|
+
|
|
242
|
+
self.send_response(200)
|
|
243
|
+
self.add_cors_headers()
|
|
244
|
+
self.end_headers()
|
|
245
|
+
self.wfile.write(b"{}")
|
|
246
|
+
result_queue.put(api_key or None)
|
|
247
|
+
|
|
248
|
+
with socket.socket() as sock:
|
|
249
|
+
sock.bind(("", 0))
|
|
250
|
+
port = sock.getsockname()[1]
|
|
251
|
+
|
|
252
|
+
server = HTTPServer(("localhost", port), LoginHandler)
|
|
253
|
+
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
yield f"http://localhost:{port}"
|
|
257
|
+
finally:
|
|
258
|
+
server.server_close()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
async def me(api_key: str) -> Me:
|
|
262
|
+
"""Gets the current user's information"""
|
|
263
|
+
async with cloud_client(api_key) as client:
|
|
264
|
+
response = await client.request("GET", "/me/")
|
|
265
|
+
response.raise_for_status()
|
|
266
|
+
|
|
267
|
+
return Me.model_validate_json(response.text)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
async def get_accounts(api_key: str) -> Sequence[Account]:
|
|
271
|
+
"""Gets the list of accounts for the current user"""
|
|
272
|
+
async with cloud_client(api_key) as client:
|
|
273
|
+
response = await client.request("GET", "/me/accounts")
|
|
274
|
+
response.raise_for_status()
|
|
275
|
+
|
|
276
|
+
return TypeAdapter(list[Account]).validate_json(response.text)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
async def get_workspaces(api_key: str) -> Sequence[Workspace]:
|
|
280
|
+
"""Gets the list of workspaces for the current user"""
|
|
281
|
+
async with cloud_client(api_key) as client:
|
|
282
|
+
response = await client.request("GET", "/me/workspaces")
|
|
283
|
+
response.raise_for_status()
|
|
284
|
+
|
|
285
|
+
workspaces = TypeAdapter(list[Workspace]).validate_json(response.text)
|
|
286
|
+
workspaces.sort(key=lambda w: w.full_handle)
|
|
287
|
+
|
|
288
|
+
return workspaces
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
async def lookup_workspace(api_key: str, workspace_id_or_slug: str) -> Workspace | None:
|
|
292
|
+
"""Looks up a workspace by ID or slug"""
|
|
293
|
+
workspace_id: UUID | None = None
|
|
294
|
+
try:
|
|
295
|
+
workspace_id = UUID(workspace_id_or_slug)
|
|
296
|
+
except ValueError:
|
|
297
|
+
pass
|
|
298
|
+
|
|
299
|
+
for workspace in await get_workspaces(api_key):
|
|
300
|
+
if workspace_id and workspace.workspace_id == workspace_id:
|
|
301
|
+
return workspace
|
|
302
|
+
if workspace.full_handle == workspace_id_or_slug:
|
|
303
|
+
return workspace
|
|
304
|
+
if workspace.workspace_handle == workspace_id_or_slug:
|
|
305
|
+
return workspace
|
|
306
|
+
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
async def prompt_for_workspace(api_key: str) -> Workspace | None:
|
|
311
|
+
"""Prompts the user to select a workspace from the list of available workspaces"""
|
|
312
|
+
workspaces = await get_workspaces(api_key)
|
|
313
|
+
|
|
314
|
+
if not workspaces:
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
if len(workspaces) == 1:
|
|
318
|
+
return workspaces[0]
|
|
319
|
+
|
|
320
|
+
selected = prompt_select_from_list(
|
|
321
|
+
"Select a workspace",
|
|
322
|
+
sorted([workspace.full_handle for workspace in workspaces]),
|
|
323
|
+
)
|
|
324
|
+
return next(
|
|
325
|
+
workspace for workspace in workspaces if workspace.full_handle == selected
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def get_from_env_or_profile(key: str) -> str | None:
|
|
330
|
+
"""Gets value from the environment or the current cloud profile"""
|
|
331
|
+
profile = get_cloud_profile() or {}
|
|
332
|
+
|
|
333
|
+
if from_env := os.environ.get(key):
|
|
334
|
+
return from_env
|
|
335
|
+
elif from_profile := profile.get(key):
|
|
336
|
+
return from_profile
|
|
337
|
+
else:
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def get_api_key() -> str | None:
|
|
342
|
+
return get_from_env_or_profile("PREFECT_API_KEY")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def get_api_url() -> str | None:
|
|
346
|
+
return get_from_env_or_profile("PREFECT_API_URL")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def load_profiles() -> dict[str, Any]:
|
|
350
|
+
"""Loads the profiles from the profiles file"""
|
|
351
|
+
profile_path = PREFECT_HOME / "profiles.toml"
|
|
352
|
+
if profile_path.exists():
|
|
353
|
+
profiles = toml.load(profile_path)
|
|
354
|
+
else:
|
|
355
|
+
profiles = {}
|
|
356
|
+
|
|
357
|
+
if "profiles" not in profiles:
|
|
358
|
+
profiles["profiles"] = {}
|
|
359
|
+
|
|
360
|
+
return profiles
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def cloud_profile_name() -> str:
|
|
364
|
+
"""Returns the name of the current cloud profile"""
|
|
365
|
+
if CLOUD_API_URL == "https://api.stg.prefect.dev/api":
|
|
366
|
+
return "prefect-cloud-stg"
|
|
367
|
+
elif CLOUD_API_URL == "https://api.prefect.dev/api":
|
|
368
|
+
return "prefect-cloud-dev"
|
|
369
|
+
elif CLOUD_API_URL.startswith("http://localhost"):
|
|
370
|
+
return "prefect-cloud-lcl"
|
|
371
|
+
else:
|
|
372
|
+
return "prefect-cloud"
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def get_cloud_profile() -> dict[str, str] | None:
|
|
376
|
+
"""Returns the current cloud profile"""
|
|
377
|
+
profile_path = PREFECT_HOME / "profiles.toml"
|
|
378
|
+
if not profile_path.exists():
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
profiles = toml.load(profile_path)
|
|
382
|
+
return profiles.get("profiles", {}).get(cloud_profile_name())
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def set_cloud_profile(api_key: str, workspace: Workspace) -> None:
|
|
386
|
+
"""Writes the current cloud profile"""
|
|
387
|
+
profile_name = cloud_profile_name()
|
|
388
|
+
profile_path = PREFECT_HOME / "profiles.toml"
|
|
389
|
+
|
|
390
|
+
profiles = load_profiles()
|
|
391
|
+
|
|
392
|
+
profiles["active"] = profile_name
|
|
393
|
+
if profile_name not in profiles["profiles"]:
|
|
394
|
+
profiles["profiles"][profile_name] = {}
|
|
395
|
+
|
|
396
|
+
profiles["profiles"][profile_name].update(
|
|
397
|
+
{
|
|
398
|
+
"PREFECT_API_KEY": api_key,
|
|
399
|
+
"PREFECT_API_URL": workspace.api_url,
|
|
400
|
+
}
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
profile_path.parent.mkdir(parents=True, exist_ok=True)
|
|
404
|
+
profile_path.write_text(toml.dumps(profiles))
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def remove_cloud_profile() -> None:
|
|
408
|
+
"""Removes the current cloud profile"""
|
|
409
|
+
profile_path = PREFECT_HOME / "profiles.toml"
|
|
410
|
+
if not profile_path.exists():
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
profiles = load_profiles()
|
|
414
|
+
profiles["profiles"].pop(cloud_profile_name(), None)
|
|
415
|
+
try:
|
|
416
|
+
profiles["active"] = list(profiles["profiles"].keys())[0]
|
|
417
|
+
profile_path.write_text(toml.dumps(profiles))
|
|
418
|
+
except IndexError:
|
|
419
|
+
profile_path.unlink()
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from prefect_cloud.auth import get_cloud_urls_without_login, sync_cloud_client
|
|
6
|
+
|
|
7
|
+
COMPLETION_CACHE = Path.home() / ".prefect" / "prefect-cloud-completions.json"
|
|
8
|
+
CACHE_TTL = 86400
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def clear_cache():
|
|
12
|
+
if COMPLETION_CACHE.exists():
|
|
13
|
+
COMPLETION_CACHE.unlink()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def complete_deployment(incomplete: str) -> list[str]:
|
|
17
|
+
_, api_url, api_key = get_cloud_urls_without_login()
|
|
18
|
+
if not api_url or not api_key:
|
|
19
|
+
return []
|
|
20
|
+
|
|
21
|
+
deployment_names = None
|
|
22
|
+
if (
|
|
23
|
+
COMPLETION_CACHE.exists()
|
|
24
|
+
and time.time() - COMPLETION_CACHE.stat().st_mtime < CACHE_TTL
|
|
25
|
+
):
|
|
26
|
+
try:
|
|
27
|
+
with open(COMPLETION_CACHE) as f:
|
|
28
|
+
cache = json.load(f)
|
|
29
|
+
deployment_names = cache["deployment_names"]
|
|
30
|
+
except (IOError, json.JSONDecodeError, KeyError):
|
|
31
|
+
deployment_names = []
|
|
32
|
+
|
|
33
|
+
if deployment_names is None:
|
|
34
|
+
with sync_cloud_client(api_key) as client:
|
|
35
|
+
response = client.post(f"{api_url}/deployments/filter")
|
|
36
|
+
response.raise_for_status()
|
|
37
|
+
deployments = response.json()
|
|
38
|
+
|
|
39
|
+
response = client.post(f"{api_url}/flows/filter")
|
|
40
|
+
response.raise_for_status()
|
|
41
|
+
flows = response.json()
|
|
42
|
+
|
|
43
|
+
flow_names = {flow["id"]: flow["name"] for flow in flows}
|
|
44
|
+
|
|
45
|
+
deployment_names = [
|
|
46
|
+
f"{flow_names[deployment['flow_id']]}/{deployment['name']}"
|
|
47
|
+
for deployment in deployments
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
COMPLETION_CACHE.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
with open(COMPLETION_CACHE, "w") as f:
|
|
52
|
+
json.dump({"deployment_names": deployment_names}, f)
|
|
53
|
+
|
|
54
|
+
return [name for name in deployment_names if name.startswith(incomplete)]
|