wmill 1.500.0__py3-none-any.whl → 1.500.2__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.
wmill/client.py
CHANGED
@@ -16,7 +16,12 @@ from typing import Dict, Any, Union, Literal
|
|
16
16
|
import httpx
|
17
17
|
|
18
18
|
from .s3_reader import S3BufferedReader, bytes_generator
|
19
|
-
from .s3_types import
|
19
|
+
from .s3_types import (
|
20
|
+
Boto3ConnectionSettings,
|
21
|
+
DuckDbConnectionSettings,
|
22
|
+
PolarsConnectionSettings,
|
23
|
+
S3Object,
|
24
|
+
)
|
20
25
|
|
21
26
|
_client: "Windmill | None" = None
|
22
27
|
|
@@ -27,7 +32,11 @@ JobStatus = Literal["RUNNING", "WAITING", "COMPLETED"]
|
|
27
32
|
|
28
33
|
class Windmill:
|
29
34
|
def __init__(self, base_url=None, token=None, workspace=None, verify=True):
|
30
|
-
base =
|
35
|
+
base = (
|
36
|
+
base_url
|
37
|
+
or os.environ.get("BASE_INTERNAL_URL")
|
38
|
+
or os.environ.get("WM_BASE_URL")
|
39
|
+
)
|
31
40
|
|
32
41
|
self.base_url = f"{base}/api"
|
33
42
|
self.token = token or os.environ.get("WM_TOKEN")
|
@@ -42,7 +51,9 @@ class Windmill:
|
|
42
51
|
|
43
52
|
self.mocked_api = self.get_mocked_api()
|
44
53
|
|
45
|
-
assert self.workspace,
|
54
|
+
assert self.workspace, (
|
55
|
+
f"workspace required as an argument or as WM_WORKSPACE environment variable"
|
56
|
+
)
|
46
57
|
|
47
58
|
def get_mocked_api(self) -> Optional[dict]:
|
48
59
|
mocked_path = os.environ.get("WM_MOCKED_API_FILE")
|
@@ -55,7 +66,10 @@ class Windmill:
|
|
55
66
|
incoming_mocked_api = json.load(f)
|
56
67
|
mocked_api = {**mocked_api, **incoming_mocked_api}
|
57
68
|
except Exception as e:
|
58
|
-
logger.warning(
|
69
|
+
logger.warning(
|
70
|
+
"Error parsing mocked API file at path %s Using empty mocked API.",
|
71
|
+
mocked_path,
|
72
|
+
)
|
59
73
|
logger.debug(e)
|
60
74
|
return mocked_api
|
61
75
|
|
@@ -165,7 +179,9 @@ class Windmill:
|
|
165
179
|
timeout = timeout.total_seconds()
|
166
180
|
|
167
181
|
job_id = self.run_script_async(path=path, hash_=hash_, args=args)
|
168
|
-
return self.wait_job(
|
182
|
+
return self.wait_job(
|
183
|
+
job_id, timeout, verbose, cleanup, assert_result_is_not_none
|
184
|
+
)
|
169
185
|
|
170
186
|
def wait_job(
|
171
187
|
self,
|
@@ -191,7 +207,9 @@ class Windmill:
|
|
191
207
|
timeout = timeout.total_seconds()
|
192
208
|
|
193
209
|
while True:
|
194
|
-
result_res = self.get(
|
210
|
+
result_res = self.get(
|
211
|
+
f"/w/{self.workspace}/jobs_u/completed/get_result_maybe/{job_id}", True
|
212
|
+
).json()
|
195
213
|
|
196
214
|
started = result_res["started"]
|
197
215
|
completed = result_res["completed"]
|
@@ -300,7 +318,9 @@ class Windmill:
|
|
300
318
|
result = variables[path]
|
301
319
|
return result
|
302
320
|
except KeyError:
|
303
|
-
logger.info(
|
321
|
+
logger.info(
|
322
|
+
f"MockedAPI present, but variable not found at {path}, falling back to real API"
|
323
|
+
)
|
304
324
|
|
305
325
|
"""Get variable from Windmill"""
|
306
326
|
return self.get(f"/w/{self.workspace}/variables/get_value/{path}").json()
|
@@ -312,7 +332,9 @@ class Windmill:
|
|
312
332
|
|
313
333
|
"""Set variable from Windmill"""
|
314
334
|
# check if variable exists
|
315
|
-
r = self.get(
|
335
|
+
r = self.get(
|
336
|
+
f"/w/{self.workspace}/variables/get/{path}", raise_for_status=False
|
337
|
+
)
|
316
338
|
if r.status_code == 404:
|
317
339
|
# create variable
|
318
340
|
self.post(
|
@@ -344,13 +366,19 @@ class Windmill:
|
|
344
366
|
except KeyError:
|
345
367
|
# NOTE: should mocked_api respect `none_if_undefined`?
|
346
368
|
if none_if_undefined:
|
347
|
-
logger.info(
|
369
|
+
logger.info(
|
370
|
+
f"resource not found at ${path}, but none_if_undefined is True, so returning None"
|
371
|
+
)
|
348
372
|
return None
|
349
|
-
logger.info(
|
373
|
+
logger.info(
|
374
|
+
f"MockedAPI present, but resource not found at ${path}, falling back to real API"
|
375
|
+
)
|
350
376
|
|
351
377
|
"""Get resource from Windmill"""
|
352
378
|
try:
|
353
|
-
return self.get(
|
379
|
+
return self.get(
|
380
|
+
f"/w/{self.workspace}/resources/get_value_interpolated/{path}"
|
381
|
+
).json()
|
354
382
|
except Exception as e:
|
355
383
|
if none_if_undefined:
|
356
384
|
return None
|
@@ -368,7 +396,9 @@ class Windmill:
|
|
368
396
|
return
|
369
397
|
|
370
398
|
# check if resource exists
|
371
|
-
r = self.get(
|
399
|
+
r = self.get(
|
400
|
+
f"/w/{self.workspace}/resources/get/{path}", raise_for_status=False
|
401
|
+
)
|
372
402
|
if r.status_code == 404:
|
373
403
|
# create resource
|
374
404
|
self.post(
|
@@ -422,14 +452,21 @@ class Windmill:
|
|
422
452
|
def set_flow_user_state(self, key: str, value: Any) -> None:
|
423
453
|
"""Set the user state of a flow at a given key"""
|
424
454
|
flow_id = self.get_root_job_id()
|
425
|
-
r = self.post(
|
455
|
+
r = self.post(
|
456
|
+
f"/w/{self.workspace}/jobs/flow/user_states/{flow_id}/{key}",
|
457
|
+
json=value,
|
458
|
+
raise_for_status=False,
|
459
|
+
)
|
426
460
|
if r.status_code == 404:
|
427
461
|
print(f"Job {flow_id} does not exist or is not a flow")
|
428
462
|
|
429
463
|
def get_flow_user_state(self, key: str) -> Any:
|
430
464
|
"""Get the user state of a flow at a given key"""
|
431
465
|
flow_id = self.get_root_job_id()
|
432
|
-
r = self.get(
|
466
|
+
r = self.get(
|
467
|
+
f"/w/{self.workspace}/jobs/flow/user_states/{flow_id}/{key}",
|
468
|
+
raise_for_status=False,
|
469
|
+
)
|
433
470
|
if r.status_code == 404:
|
434
471
|
print(f"Job {flow_id} does not exist or is not a flow")
|
435
472
|
return None
|
@@ -451,11 +488,15 @@ class Windmill:
|
|
451
488
|
try:
|
452
489
|
raw_obj = self.post(
|
453
490
|
f"/w/{self.workspace}/job_helpers/v2/duckdb_connection_settings",
|
454
|
-
json={}
|
491
|
+
json={}
|
492
|
+
if s3_resource_path == ""
|
493
|
+
else {"s3_resource_path": s3_resource_path},
|
455
494
|
).json()
|
456
495
|
return DuckDbConnectionSettings(raw_obj)
|
457
496
|
except JSONDecodeError as e:
|
458
|
-
raise Exception(
|
497
|
+
raise Exception(
|
498
|
+
"Could not generate DuckDB S3 connection settings from the provided resource"
|
499
|
+
) from e
|
459
500
|
|
460
501
|
def get_polars_connection_settings(
|
461
502
|
self,
|
@@ -468,11 +509,15 @@ class Windmill:
|
|
468
509
|
try:
|
469
510
|
raw_obj = self.post(
|
470
511
|
f"/w/{self.workspace}/job_helpers/v2/polars_connection_settings",
|
471
|
-
json={}
|
512
|
+
json={}
|
513
|
+
if s3_resource_path == ""
|
514
|
+
else {"s3_resource_path": s3_resource_path},
|
472
515
|
).json()
|
473
516
|
return PolarsConnectionSettings(raw_obj)
|
474
517
|
except JSONDecodeError as e:
|
475
|
-
raise Exception(
|
518
|
+
raise Exception(
|
519
|
+
"Could not generate Polars S3 connection settings from the provided resource"
|
520
|
+
) from e
|
476
521
|
|
477
522
|
def get_boto3_connection_settings(
|
478
523
|
self,
|
@@ -485,11 +530,15 @@ class Windmill:
|
|
485
530
|
try:
|
486
531
|
s3_resource = self.post(
|
487
532
|
f"/w/{self.workspace}/job_helpers/v2/s3_resource_info",
|
488
|
-
json={}
|
533
|
+
json={}
|
534
|
+
if s3_resource_path == ""
|
535
|
+
else {"s3_resource_path": s3_resource_path},
|
489
536
|
).json()
|
490
537
|
return self.__boto3_connection_settings(s3_resource)
|
491
538
|
except JSONDecodeError as e:
|
492
|
-
raise Exception(
|
539
|
+
raise Exception(
|
540
|
+
"Could not generate Boto3 S3 connection settings from the provided resource"
|
541
|
+
) from e
|
493
542
|
|
494
543
|
def load_s3_file(self, s3object: S3Object, s3_resource_path: str | None) -> bytes:
|
495
544
|
"""
|
@@ -506,7 +555,9 @@ class Windmill:
|
|
506
555
|
with self.load_s3_file_reader(s3object, s3_resource_path) as file_reader:
|
507
556
|
return file_reader.read()
|
508
557
|
|
509
|
-
def load_s3_file_reader(
|
558
|
+
def load_s3_file_reader(
|
559
|
+
self, s3object: S3Object, s3_resource_path: str | None
|
560
|
+
) -> BufferedReader:
|
510
561
|
"""
|
511
562
|
Load a file from the workspace s3 bucket and returns the bytes stream.
|
512
563
|
|
@@ -565,7 +616,11 @@ class Windmill:
|
|
565
616
|
query_params["file_key"] = s3object["s3"]
|
566
617
|
if s3_resource_path is not None and s3_resource_path != "":
|
567
618
|
query_params["s3_resource_path"] = s3_resource_path
|
568
|
-
if
|
619
|
+
if (
|
620
|
+
s3object is not None
|
621
|
+
and "storage" in s3object
|
622
|
+
and s3object["storage"] is not None
|
623
|
+
):
|
569
624
|
query_params["storage"] = s3object["storage"]
|
570
625
|
if content_type is not None:
|
571
626
|
query_params["content_type"] = content_type
|
@@ -576,7 +631,10 @@ class Windmill:
|
|
576
631
|
# need a vanilla client b/c content-type is not application/json here
|
577
632
|
response = httpx.post(
|
578
633
|
f"{self.base_url}/w/{self.workspace}/job_helpers/upload_s3_file",
|
579
|
-
headers={
|
634
|
+
headers={
|
635
|
+
"Authorization": f"Bearer {self.token}",
|
636
|
+
"Content-Type": "application/octet-stream",
|
637
|
+
},
|
580
638
|
params=query_params,
|
581
639
|
content=content_payload,
|
582
640
|
verify=self.verify,
|
@@ -587,16 +645,23 @@ class Windmill:
|
|
587
645
|
return S3Object(s3=response["file_key"])
|
588
646
|
|
589
647
|
def sign_s3_objects(self, s3_objects: list[S3Object]) -> list[S3Object]:
|
590
|
-
return self.post(
|
648
|
+
return self.post(
|
649
|
+
f"/w/{self.workspace}/apps/sign_s3_objects", json={"s3_objects": s3_objects}
|
650
|
+
).json()
|
591
651
|
|
592
652
|
def sign_s3_object(self, s3_object: S3Object) -> S3Object:
|
593
|
-
return self.post(
|
653
|
+
return self.post(
|
654
|
+
f"/w/{self.workspace}/apps/sign_s3_objects",
|
655
|
+
json={"s3_objects": [s3_object]},
|
656
|
+
).json()[0]
|
594
657
|
|
595
658
|
def __boto3_connection_settings(self, s3_resource) -> Boto3ConnectionSettings:
|
596
659
|
endpoint_url_prefix = "https://" if s3_resource["useSSL"] else "http://"
|
597
660
|
return Boto3ConnectionSettings(
|
598
661
|
{
|
599
|
-
"endpoint_url": "{}{}".format(
|
662
|
+
"endpoint_url": "{}{}".format(
|
663
|
+
endpoint_url_prefix, s3_resource["endPoint"]
|
664
|
+
),
|
600
665
|
"region_name": s3_resource["region"],
|
601
666
|
"use_ssl": s3_resource["useSSL"],
|
602
667
|
"aws_access_key_id": s3_resource["accessKey"],
|
@@ -614,7 +679,9 @@ class Windmill:
|
|
614
679
|
|
615
680
|
@property
|
616
681
|
def state_path(self) -> str:
|
617
|
-
state_path = os.environ.get(
|
682
|
+
state_path = os.environ.get(
|
683
|
+
"WM_STATE_PATH_NEW", os.environ.get("WM_STATE_PATH")
|
684
|
+
)
|
618
685
|
if state_path is None:
|
619
686
|
raise Exception("State path not found")
|
620
687
|
return state_path
|
@@ -686,7 +753,7 @@ class Windmill:
|
|
686
753
|
) -> None:
|
687
754
|
"""
|
688
755
|
Sends an interactive approval request via Slack, allowing optional customization of the message, approver, and form fields.
|
689
|
-
|
756
|
+
|
690
757
|
**[Enterprise Edition Only]** To include form fields in the Slack approval request, use the "Advanced -> Suspend -> Form" functionality.
|
691
758
|
Learn more at: https://www.windmill.dev/docs/flows/flow_approval#form
|
692
759
|
|
@@ -702,10 +769,10 @@ class Windmill:
|
|
702
769
|
:type default_args_json: dict, optional
|
703
770
|
:param dynamic_enums_json: Optional dictionary overriding the enum default values of enum form fields.
|
704
771
|
:type dynamic_enums_json: dict, optional
|
705
|
-
|
772
|
+
|
706
773
|
:raises Exception: If the function is not called within a flow or flow preview.
|
707
774
|
:raises Exception: If the required flow job or flow step environment variables are not set.
|
708
|
-
|
775
|
+
|
709
776
|
:return: None
|
710
777
|
|
711
778
|
**Usage Example:**
|
@@ -759,13 +826,26 @@ class Windmill:
|
|
759
826
|
Indeed, in the viewer context WM_USERNAME is set to the username of the viewer but WM_EMAIL is set to the email of the creator of the app.
|
760
827
|
"""
|
761
828
|
return self.get(f"/w/{self.workspace}/users/username_to_email/{username}").text
|
762
|
-
|
763
829
|
|
764
|
-
def send_teams_message(
|
830
|
+
def send_teams_message(
|
831
|
+
self,
|
832
|
+
conversation_id: str,
|
833
|
+
text: str,
|
834
|
+
success: bool = True,
|
835
|
+
card_block: dict = None,
|
836
|
+
):
|
765
837
|
"""
|
766
838
|
Send a message to a Microsoft Teams conversation with conversation_id, where success is used to style the message
|
767
839
|
"""
|
768
|
-
return self.post(
|
840
|
+
return self.post(
|
841
|
+
f"/teams/activities",
|
842
|
+
json={
|
843
|
+
"conversation_id": conversation_id,
|
844
|
+
"text": text,
|
845
|
+
"success": success,
|
846
|
+
"card_block": card_block,
|
847
|
+
},
|
848
|
+
)
|
769
849
|
|
770
850
|
|
771
851
|
def init_global_client(f):
|
@@ -914,7 +994,9 @@ def get_job_status(job_id: str) -> JobStatus:
|
|
914
994
|
|
915
995
|
@init_global_client
|
916
996
|
def get_result(job_id: str, assert_result_is_not_none=True) -> Dict[str, Any]:
|
917
|
-
return _client.get_result(
|
997
|
+
return _client.get_result(
|
998
|
+
job_id=job_id, assert_result_is_not_none=assert_result_is_not_none
|
999
|
+
)
|
918
1000
|
|
919
1001
|
|
920
1002
|
@init_global_client
|
@@ -949,15 +1031,21 @@ def load_s3_file(s3object: S3Object, s3_resource_path: str | None = None) -> byt
|
|
949
1031
|
"""
|
950
1032
|
Load the entire content of a file stored in S3 as bytes
|
951
1033
|
"""
|
952
|
-
return _client.load_s3_file(
|
1034
|
+
return _client.load_s3_file(
|
1035
|
+
s3object, s3_resource_path if s3_resource_path != "" else None
|
1036
|
+
)
|
953
1037
|
|
954
1038
|
|
955
1039
|
@init_global_client
|
956
|
-
def load_s3_file_reader(
|
1040
|
+
def load_s3_file_reader(
|
1041
|
+
s3object: S3Object, s3_resource_path: str | None = None
|
1042
|
+
) -> BufferedReader:
|
957
1043
|
"""
|
958
1044
|
Load the content of a file stored in S3
|
959
1045
|
"""
|
960
|
-
return _client.load_s3_file_reader(
|
1046
|
+
return _client.load_s3_file_reader(
|
1047
|
+
s3object, s3_resource_path if s3_resource_path != "" else None
|
1048
|
+
)
|
961
1049
|
|
962
1050
|
|
963
1051
|
@init_global_client
|
@@ -977,7 +1065,13 @@ def write_s3_file(
|
|
977
1065
|
and content_type: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
|
978
1066
|
|
979
1067
|
"""
|
980
|
-
return _client.write_s3_file(
|
1068
|
+
return _client.write_s3_file(
|
1069
|
+
s3object,
|
1070
|
+
file_content,
|
1071
|
+
s3_resource_path if s3_resource_path != "" else None,
|
1072
|
+
content_type,
|
1073
|
+
content_disposition,
|
1074
|
+
)
|
981
1075
|
|
982
1076
|
|
983
1077
|
@init_global_client
|
@@ -1126,6 +1220,7 @@ def get_state_path() -> str:
|
|
1126
1220
|
def get_resume_urls(approver: str = None) -> dict:
|
1127
1221
|
return _client.get_resume_urls(approver)
|
1128
1222
|
|
1223
|
+
|
1129
1224
|
@init_global_client
|
1130
1225
|
def request_interactive_slack_approval(
|
1131
1226
|
slack_resource_path: str,
|
@@ -1144,10 +1239,14 @@ def request_interactive_slack_approval(
|
|
1144
1239
|
dynamic_enums_json=dynamic_enums_json,
|
1145
1240
|
)
|
1146
1241
|
|
1242
|
+
|
1147
1243
|
@init_global_client
|
1148
|
-
def send_teams_message(
|
1244
|
+
def send_teams_message(
|
1245
|
+
conversation_id: str, text: str, success: bool, card_block: dict = None
|
1246
|
+
):
|
1149
1247
|
return _client.send_teams_message(conversation_id, text, success, card_block)
|
1150
1248
|
|
1249
|
+
|
1151
1250
|
@init_global_client
|
1152
1251
|
def cancel_running() -> dict:
|
1153
1252
|
"""Cancel currently running executions of the same script."""
|
@@ -1190,7 +1289,10 @@ def task(*args, **kwargs):
|
|
1190
1289
|
from inspect import signature
|
1191
1290
|
|
1192
1291
|
def f(func, tag: str | None = None):
|
1193
|
-
if
|
1292
|
+
if (
|
1293
|
+
os.environ.get("WM_JOB_ID") is None
|
1294
|
+
or os.environ.get("MAIN_OVERRIDE") == func.__name__
|
1295
|
+
):
|
1194
1296
|
|
1195
1297
|
def inner(*args, **kwargs):
|
1196
1298
|
return func(*args, **kwargs)
|
@@ -1,8 +1,8 @@
|
|
1
1
|
wmill/__init__.py,sha256=nGZnQPezTdrBnBW1D0JqUtm75Gdf_xi3tAcPGwHRZ5A,46
|
2
|
-
wmill/client.py,sha256=
|
2
|
+
wmill/client.py,sha256=ZtFtIqi6VKPP3fQgofylkecxVUerNffRH2mWAFDuodY,43537
|
3
3
|
wmill/py.typed,sha256=8PjyZ1aVoQpRVvt71muvuq5qE-jTFZkK-GLHkhdebmc,26
|
4
4
|
wmill/s3_reader.py,sha256=izHlg2Xsg0Sr_LkDDEC35VuEijJcuPBDIm-xj21KsgU,1668
|
5
5
|
wmill/s3_types.py,sha256=S5w6fVAai5Adm1MxZoxF21R-EE5-wRfGzXBK72-FZvE,1199
|
6
|
-
wmill-1.500.
|
7
|
-
wmill-1.500.
|
8
|
-
wmill-1.500.
|
6
|
+
wmill-1.500.2.dist-info/METADATA,sha256=dGI7Z8klzYZpqUbnIwMhR-YvOzttZAjfioct0lsu5l4,2693
|
7
|
+
wmill-1.500.2.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
|
8
|
+
wmill-1.500.2.dist-info/RECORD,,
|
File without changes
|