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 Boto3ConnectionSettings, DuckDbConnectionSettings, PolarsConnectionSettings, S3Object
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 = base_url or os.environ.get("BASE_INTERNAL_URL") or os.environ.get("WM_BASE_URL")
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, f"workspace required as an argument or as WM_WORKSPACE environment variable"
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("Error parsing mocked API file at path %s Using empty mocked API.", mocked_path)
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(job_id, timeout, verbose, cleanup, assert_result_is_not_none)
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(f"/w/{self.workspace}/jobs_u/completed/get_result_maybe/{job_id}", True).json()
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(f"MockedAPI present, but variable not found at {path}, falling back to real API")
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(f"/w/{self.workspace}/variables/get/{path}", raise_for_status=False)
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(f"resource not found at ${path}, but none_if_undefined is True, so returning None")
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(f"MockedAPI present, but resource not found at ${path}, falling back to real API")
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(f"/w/{self.workspace}/resources/get_value_interpolated/{path}").json()
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(f"/w/{self.workspace}/resources/get/{path}", raise_for_status=False)
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(f"/w/{self.workspace}/jobs/flow/user_states/{flow_id}/{key}", json=value, raise_for_status=False)
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(f"/w/{self.workspace}/jobs/flow/user_states/{flow_id}/{key}", raise_for_status=False)
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={} if s3_resource_path == "" else {"s3_resource_path": s3_resource_path},
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("Could not generate DuckDB S3 connection settings from the provided resource") from e
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={} if s3_resource_path == "" else {"s3_resource_path": s3_resource_path},
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("Could not generate Polars S3 connection settings from the provided resource") from e
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={} if s3_resource_path == "" else {"s3_resource_path": s3_resource_path},
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("Could not generate Boto3 S3 connection settings from the provided resource") from e
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(self, s3object: S3Object, s3_resource_path: str | None) -> BufferedReader:
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 s3object is not None and "storage" in s3object and s3object["storage"] is not None:
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={"Authorization": f"Bearer {self.token}", "Content-Type": "application/octet-stream"},
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(f"/w/{self.workspace}/apps/sign_s3_objects", json={"s3_objects": s3_objects}).json()
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(f"/w/{self.workspace}/apps/sign_s3_objects", json={"s3_objects": [s3_object]}).json()[0]
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(endpoint_url_prefix, s3_resource["endPoint"]),
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("WM_STATE_PATH_NEW", os.environ.get("WM_STATE_PATH"))
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(self, conversation_id: str, text: str, success: bool = True, card_block: dict = None):
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(f"/teams/activities", json={"conversation_id": conversation_id, "text": text, "success": success, "card_block": card_block})
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(job_id=job_id, assert_result_is_not_none=assert_result_is_not_none)
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(s3object, s3_resource_path if s3_resource_path != "" else None)
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(s3object: S3Object, s3_resource_path: str | None = None) -> BufferedReader:
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(s3object, s3_resource_path if s3_resource_path != "" else None)
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(s3object, file_content, s3_resource_path if s3_resource_path != "" else None, content_type, content_disposition)
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(conversation_id: str, text: str, success: bool, card_block: dict = None):
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 os.environ.get("WM_JOB_ID") is None or os.environ.get("MAIN_OVERRIDE") == func.__name__:
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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wmill
3
- Version: 1.500.0
3
+ Version: 1.500.2
4
4
  Summary: A client library for accessing Windmill server wrapping the Windmill client API
5
5
  Home-page: https://windmill.dev
6
6
  License: Apache-2.0
@@ -1,8 +1,8 @@
1
1
  wmill/__init__.py,sha256=nGZnQPezTdrBnBW1D0JqUtm75Gdf_xi3tAcPGwHRZ5A,46
2
- wmill/client.py,sha256=bwWDX_TKjddIlA-guczp32kT_Z0nLUGZNyZmm4LCNvM,42371
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.0.dist-info/METADATA,sha256=pF4mKo8SYwEfvTEA9dnHt3k8VpGXH4XbfHmp0AfxV7o,2693
7
- wmill-1.500.0.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
8
- wmill-1.500.0.dist-info/RECORD,,
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,,