pyfirecrest 3.2.2__tar.gz → 3.3.1__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.
Files changed (113) hide show
  1. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/PKG-INFO +3 -3
  2. pyfirecrest-3.3.1/docs/source/authorization.rst +121 -0
  3. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/FirecrestException.py +12 -0
  4. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/__init__.py +28 -14
  5. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/cli2/__init__.py +19 -0
  6. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/v1/__init__.py +12 -1
  7. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/v2/__init__.py +9 -0
  8. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/v2/_async/Client.py +81 -33
  9. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/v2/_sync/Client.py +90 -42
  10. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/pyproject.toml +2 -2
  11. pyfirecrest-3.2.2/docs/source/authorization.rst +0 -47
  12. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/.github/workflows/python-publish.yml +0 -0
  13. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/.github/workflows/testing.yml +0 -0
  14. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/.gitignore +0 -0
  15. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/.mypy.ini +0 -0
  16. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/.readthedocs.yml +0 -0
  17. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/LICENSE +0 -0
  18. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/README.md +0 -0
  19. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/async-example.md +0 -0
  20. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/Makefile +0 -0
  21. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/conf.py +0 -0
  22. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/index.rst +0 -0
  23. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/reference_async.rst +0 -0
  24. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/reference_async_v2.rst +0 -0
  25. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/reference_auth.rst +0 -0
  26. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/reference_basic.rst +0 -0
  27. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/reference_cli.rst +0 -0
  28. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/reference_sync_v2.rst +0 -0
  29. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/reference_v1_index.rst +0 -0
  30. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/reference_v2_index.rst +0 -0
  31. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/tutorial_async.rst +0 -0
  32. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/tutorial_basic_v1.rst +0 -0
  33. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/tutorial_basic_v2.rst +0 -0
  34. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/tutorial_cli.rst +0 -0
  35. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/tutorial_errors.rst +0 -0
  36. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/tutorial_index.rst +0 -0
  37. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/tutorial_logging.rst +0 -0
  38. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/Authorization.py +0 -0
  39. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/cli/__init__.py +0 -0
  40. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/cli_script.py +0 -0
  41. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/path.py +0 -0
  42. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/py.typed +0 -0
  43. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/types.py +0 -0
  44. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/utilities.py +0 -0
  45. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/v1/AsyncClient.py +0 -0
  46. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/v1/AsyncExternalStorage.py +0 -0
  47. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/v1/BasicClient.py +0 -0
  48. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/v1/ExternalStorage.py +0 -0
  49. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/v2/_async/__init__.py +0 -0
  50. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/common.py +0 -0
  51. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/context.py +0 -0
  52. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_authorisation.py +0 -0
  53. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_compute.py +0 -0
  54. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_compute_async.py +0 -0
  55. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_extras.py +0 -0
  56. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_extras_async.py +0 -0
  57. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_status.py +0 -0
  58. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_status_async.py +0 -0
  59. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_storage.py +0 -0
  60. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_storage_async.py +0 -0
  61. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_utilities.py +0 -0
  62. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_utilities_async.py +0 -0
  63. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/common.py +0 -0
  64. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/context_v2.py +0 -0
  65. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/handlers.py +0 -0
  66. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/checksum.json +0 -0
  67. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/chmod.json +0 -0
  68. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/chown.json +0 -0
  69. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/chown_not_permitted.json +0 -0
  70. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/compress.json +0 -0
  71. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/delete.json +0 -0
  72. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/extract.json +0 -0
  73. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/file.json +0 -0
  74. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/head.json +0 -0
  75. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/head_bytes.json +0 -0
  76. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/head_bytes_exclude_trailing.json +0 -0
  77. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/head_lines.json +0 -0
  78. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/head_lines_exclude_trailing.json +0 -0
  79. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/job_info.json +0 -0
  80. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/job_metadata.json +0 -0
  81. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/job_submit.json +0 -0
  82. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls.json +0 -0
  83. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls_dereference.json +0 -0
  84. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls_hidden.json +0 -0
  85. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls_home.json +0 -0
  86. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls_home_dereference.json +0 -0
  87. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls_home_hidden.json +0 -0
  88. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls_home_recursive.json +0 -0
  89. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls_home_uid.json +0 -0
  90. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls_invalid_path.json +0 -0
  91. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls_recursive.json +0 -0
  92. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls_uid.json +0 -0
  93. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/mkdir.json +0 -0
  94. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/nodes.json +0 -0
  95. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/partitions.json +0 -0
  96. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/reservations.json +0 -0
  97. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/rm.json +0 -0
  98. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/stat.json +0 -0
  99. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/stat_dereference.json +0 -0
  100. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/symlink.json +0 -0
  101. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/systems.json +0 -0
  102. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/tail.json +0 -0
  103. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/tail_bytes.json +0 -0
  104. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/tail_bytes_exclude_beginning.json +0 -0
  105. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/tail_lines.json +0 -0
  106. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/tail_lines_exclude_beginning.json +0 -0
  107. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/userinfo.json +0 -0
  108. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/view.json +0 -0
  109. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/test_v2_async.py +0 -0
  110. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/test_v2_cli.py +0 -0
  111. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/test_v2_sync.py +0 -0
  112. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tox.ini +0 -0
  113. {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/utils/run_unasync.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyfirecrest
3
- Version: 3.2.2
3
+ Version: 3.3.1
4
4
  Summary: pyFirecrest is a python wrapper for FirecREST
5
5
  Author: CSCS Swiss National Supercomputing Center
6
6
  Maintainer-email: Eirini Koutsaniti <eirini.koutsaniti@cscs.ch>, Juan Pablo Dorsch <juanpablo.dorsch@cscs.ch>
@@ -12,10 +12,10 @@ Classifier: Programming Language :: Python :: 3.9
12
12
  Classifier: License :: OSI Approved :: BSD License
13
13
  Classifier: Operating System :: OS Independent
14
14
  License-File: LICENSE
15
- Requires-Dist: aiofiles~=23.2.1
15
+ Requires-Dist: aiofiles>=23.2.1
16
16
  Requires-Dist: requests>=2.14.0
17
17
  Requires-Dist: PyJWT>=2.4.0
18
- Requires-Dist: typer[all]~=0.7.0
18
+ Requires-Dist: typer[all]>=0.7.0,<1.0.0
19
19
  Requires-Dist: packaging>=21.0
20
20
  Requires-Dist: httpx>=0.24.0
21
21
  Requires-Dist: PyYAML>=5.1
@@ -0,0 +1,121 @@
1
+ Authorization
2
+ =============
3
+
4
+ For every request to FirecREST you need to have a valid access token.
5
+ This will enable your application to have access to the requested resources.
6
+
7
+ The pyFirecREST Authorization Object
8
+ ------------------------------------
9
+
10
+ You can take care of the access token by yourself any way you want, or even better use a python library to take care of this for you, depending on the grant type.
11
+ What pyFirecREST will need in the end is only a python object with the method ``get_access_token()``, that when called will provide a valid access token.
12
+
13
+ Let's say for example you have somehow obtained a long-lasting access token.
14
+ The Authorization class you would need to make and give to Firecrest would look like this:
15
+
16
+ .. code-block:: Python
17
+
18
+ class MyAuthorizationClass:
19
+ def __init__(self):
20
+ pass
21
+
22
+ def get_access_token(self):
23
+ return <TOKEN>
24
+
25
+ If this is your case you can move on to the next session, where you can see how to use this Authorization object, in order to make your requests.
26
+
27
+ If you want to use the Client Credentials authorization grant, you can use the ``ClientCredentialsAuth`` class from pyFirecREST and setup the authorization object like this:
28
+
29
+ .. code-block:: Python
30
+
31
+ import firecrest as f7t
32
+
33
+ keycloak = f7t.ClientCredentialsAuth(
34
+ <client_id>, <client_secret>, <token_uri>
35
+ )
36
+
37
+ Device Authorization Grant Workflow
38
+ -----------------------------------
39
+
40
+ When your application cannot open a browser or embed a user-agent, you can use the OAuth 2.0 Device Authorization Grant.
41
+ In this flow, your app is responsible for:
42
+ 1. Requesting a device code & user code from the token endpoint
43
+ 2. Instructing the user to visit a verification URL and enter the code
44
+ 3. Polling the token endpoint until the user has authorized
45
+ 4. Caching and refreshing the access token for future calls
46
+
47
+ This workflow is not handled by pyFirecREST directly, but you can implement it using the `requests` library or any other HTTP client of your choice.
48
+ The `Authorization` object you hand to pyFirecREST needs the `get_access_token()` method, but it would also have orchestrate to workflow behind the scenes.
49
+
50
+ Here we provide an example implementation using `requests`.
51
+ We chose to inform the user about the verification URL and user code by simply printing it, but you should adapt this to the needs of your application.
52
+
53
+ .. code-block:: python
54
+
55
+ import time
56
+ import requests
57
+
58
+ class DeviceAuthorizationAuth:
59
+ def __init__(self, client_id, device_uri, token_uri, scope=None):
60
+ self.client_id = client_id
61
+ self.device_uri = device_uri
62
+ self.token_uri = token_uri
63
+ self.scope = scope or "openid"
64
+ self._token_data = None
65
+
66
+ def _start_device_flow(self):
67
+ resp = requests.post(self.device_uri, data={
68
+ "client_id": self.client_id,
69
+ "scope": self.scope,
70
+ })
71
+ resp.raise_for_status()
72
+ return resp.json()
73
+
74
+ def _poll_for_token(self, device_code, interval, expires_in):
75
+ deadline = time.time() + expires_in
76
+ while time.time() < deadline:
77
+ resp = requests.post(self.token_uri, data={
78
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
79
+ "device_code": device_code,
80
+ "client_id": self.client_id,
81
+ })
82
+ if resp.status_code == 200:
83
+ return resp.json()
84
+
85
+ time.sleep(interval)
86
+ raise RuntimeError("User did not authorize in time")
87
+
88
+ def get_access_token(self):
89
+ # If we already have a (non-expired) token, reuse it
90
+ if self._token_data and time.time() < self._token_data["expires_at"]:
91
+ return self._token_data["access_token"]
92
+
93
+ # 1) Start device flow
94
+ flow = self._start_device_flow()
95
+ print(f"Go to {flow['verification_uri']} and enter code {flow['user_code']}")
96
+
97
+ # 2) Poll until the user completes authorization
98
+ token = self._poll_for_token(
99
+ device_code=flow["device_code"],
100
+ interval=flow.get("interval", 5),
101
+ expires_in=flow.get("expires_in", 600),
102
+ )
103
+
104
+ # 3) Compute absolute expiry & cache
105
+ token["expires_at"] = time.time() + token["expires_in"]
106
+ self._token_data = token
107
+ return token["access_token"]
108
+
109
+ # Usage with pyFirecREST
110
+ import firecrest as f7t
111
+
112
+ auth = DeviceAuthorizationAuth(
113
+ client_id="YOUR_CLIENT_ID",
114
+ device_uri="https://auth.example.com/device",
115
+ token_uri="https://auth.example.com/token",
116
+ scope="openid profile",
117
+ )
118
+ firecrest = f7t.FirecrestClient(url="https://api.firecrest.example.com", auth=auth)
119
+
120
+
121
+ In a similar way you can reuse other packages to support different grant types, like `Flask-OIDC <https://flask-oidc.readthedocs.io/>`__.
@@ -135,6 +135,18 @@ class TransferJobFailedException(Exception):
135
135
  )
136
136
 
137
137
 
138
+ class JobTimeoutException(Exception):
139
+ """Exception when the job exceeds the user-defined timeout"""
140
+
141
+ def __init__(self, jobid):
142
+ self._jobid = jobid
143
+
144
+ def __str__(self):
145
+ return (
146
+ f"Job {self._jobid} has exceeded the user-defined timeout."
147
+ )
148
+
149
+
138
150
  class TransferJobTimeoutException(TransferJobFailedException):
139
151
  """Exception when the transfer job exceeds the user-defined timeout"""
140
152
 
@@ -5,20 +5,6 @@
5
5
  # SPDX-License-Identifier: BSD-3-Clause
6
6
  #
7
7
  import sys
8
-
9
-
10
- __version__ = "3.2.2"
11
- __app_name__ = "firecrest"
12
- MIN_PYTHON_VERSION = (3, 7, 0)
13
-
14
- # Check python version
15
- if sys.version_info[:3] < MIN_PYTHON_VERSION:
16
- sys.stderr.write(
17
- "Unsupported Python version: "
18
- "Python >= %d.%d.%d is required\n" % MIN_PYTHON_VERSION
19
- )
20
- sys.exit(1)
21
-
22
8
  from . import v1, v2
23
9
  from firecrest.Authorization import ClientCredentialsAuth
24
10
  from firecrest.FirecrestException import (
@@ -31,3 +17,31 @@ from firecrest.FirecrestException import (
31
17
  UnauthorizedException,
32
18
  UnexpectedStatusException,
33
19
  )
20
+
21
+ __version__ = "3.3.1"
22
+ __app_name__ = "firecrest"
23
+ MIN_PYTHON_VERSION = (3, 7, 0)
24
+
25
+ __all__ = [
26
+ "v1",
27
+ "v2",
28
+ "ClientCredentialsAuth",
29
+ "ClientsCredentialsException",
30
+ "FirecrestException",
31
+ "HeaderException",
32
+ "NotImplementedOnAPIversion",
33
+ "StorageDownloadException",
34
+ "StorageUploadException",
35
+ "UnauthorizedException",
36
+ "UnexpectedStatusException",
37
+ "__version__",
38
+ "__app_name__",
39
+ ]
40
+
41
+ # Check python version
42
+ if sys.version_info[:3] < MIN_PYTHON_VERSION:
43
+ sys.stderr.write(
44
+ "Unsupported Python version: "
45
+ "Python >= %d.%d.%d is required\n" % MIN_PYTHON_VERSION
46
+ )
47
+ sys.exit(1)
@@ -850,6 +850,25 @@ def job_info(
850
850
  raise typer.Exit(code=1)
851
851
 
852
852
 
853
+ @app.command(rich_help_panel="Compute commands")
854
+ def wait_for_job(
855
+ system: str = typer.Option(
856
+ ..., "-s", "--system", help="The name of the system.", envvar="FIRECREST_SYSTEM"
857
+ ),
858
+ jobid: str = typer.Argument(
859
+ ..., help="Job id of the job to wait for."
860
+ ),
861
+ ):
862
+ """Wait for a job to complete. It will return the job information when the job is completed.
863
+ """
864
+ try:
865
+ result = client.wait_for_job(system, jobid)
866
+ console.print(json.dumps(result, indent=4))
867
+ except Exception as e:
868
+ examine_exeption(e)
869
+ raise typer.Exit(code=1)
870
+
871
+
853
872
  @app.command(rich_help_panel="Compute commands")
854
873
  def job_metadata(
855
874
  system: str = typer.Option(
@@ -15,5 +15,16 @@ from firecrest.v1.BasicClient import Firecrest
15
15
  from firecrest.v1.ExternalStorage import (
16
16
  ExternalDownload,
17
17
  ExternalUpload,
18
- ExternalStorage)
18
+ ExternalStorage,
19
+ )
19
20
 
21
+ __all__ = [
22
+ "AsyncFirecrest",
23
+ "AsyncExternalDownload",
24
+ "AsyncExternalUpload",
25
+ "AsyncExternalStorage",
26
+ "Firecrest",
27
+ "ExternalDownload",
28
+ "ExternalUpload",
29
+ "ExternalStorage",
30
+ ]
@@ -15,3 +15,12 @@ from firecrest.v2._sync.Client import ( # noqa
15
15
  ExternalUpload,
16
16
  Firecrest,
17
17
  )
18
+
19
+ __all__ = [
20
+ "AsyncExternalDownload",
21
+ "AsyncExternalUpload",
22
+ "AsyncFirecrest",
23
+ "ExternalDownload",
24
+ "ExternalUpload",
25
+ "Firecrest",
26
+ ]
@@ -26,6 +26,7 @@ from firecrest.utilities import (
26
26
  )
27
27
  from firecrest.FirecrestException import (
28
28
  FirecrestException,
29
+ JobTimeoutException,
29
30
  MultipartUploadException,
30
31
  TransferJobFailedException,
31
32
  TransferJobTimeoutException,
@@ -931,15 +932,83 @@ class AsyncFirecrest:
931
932
  )
932
933
  self._check_response(resp, 204)
933
934
 
934
- async def _wait_for_transfer_job(self, job_info, timeout=None):
935
+ async def _wait_for_transfer_job(
936
+ self,
937
+ job_info: dict,
938
+ timeout: Optional[float] = None
939
+ ) -> None:
940
+ job_id = job_info["transferJob"]["jobId"]
941
+ system_name = job_info["transferJob"]["system"]
942
+ try:
943
+ await self.wait_for_job(
944
+ system_name,
945
+ job_id,
946
+ timeout=timeout
947
+ )
948
+ except JobTimeoutException:
949
+ self.log(
950
+ logging.DEBUG,
951
+ f"Transfer job {job_id} timed out"
952
+ )
953
+ raise TransferJobTimeoutException(job_info)
954
+
955
+ try:
956
+ # If job is cancelled before it starts running, the log file will
957
+ # not be available
958
+ stdout_file = await self.view(
959
+ system_name,
960
+ job_info["transferJob"]["logs"]["outputLog"]
961
+ )
962
+ except FirecrestException as e:
963
+ if (
964
+ e.responses[-1].status_code == 404 and
965
+ "No such file or directory" in e.responses[-1].json()['message']
966
+ ):
967
+ raise TransferJobFailedException(
968
+ job_info, file_not_found=True
969
+ )
970
+ else:
971
+ raise e
972
+
973
+ if (
974
+ "Files were successfully" not in stdout_file and
975
+ "File was successfully" not in stdout_file and
976
+ "Multipart file upload successfully completed" not in stdout_file
977
+ ):
978
+ raise TransferJobFailedException(job_info)
979
+
980
+ async def wait_for_job(
981
+ self,
982
+ system_name: str,
983
+ job_id: str,
984
+ timeout: Optional[float] = None,
985
+ not_found_timeout: float = 20.0
986
+ ) -> List[Any]:
987
+ """Wait for a job to complete. When the job is completed, it will
988
+ return the job information.
989
+ :param system_name: the system name where the filesystem belongs to
990
+ :param job_id: the ID of the job to wait for
991
+ :param timeout: the maximum time to wait for the job to complete
992
+ in seconds. If the timeout is set and the job is not
993
+ completed within this time, it will be cancelled.
994
+ :param not_found_timeout: the maximum time to wait for the job to be
995
+ available in the scheduler, in seconds.
996
+ Slurm or other scheduler will need a few
997
+ seconds before updating the database. If
998
+ the job is not found within this time, the
999
+ function will raise an exception.
1000
+ :calls: GET `/jobs/{system_name}/{job_id}`
1001
+ """
935
1002
  if timeout:
936
1003
  timeout_time = asyncio.get_event_loop().time() + timeout
937
1004
 
938
- job_id = job_info["transferJob"]["jobId"]
939
- system_name = job_info["transferJob"]["system"]
1005
+ not_found_timeout_time = (
1006
+ asyncio.get_event_loop().time() + not_found_timeout
1007
+ )
1008
+
940
1009
  self.log(
941
1010
  logging.DEBUG,
942
- f"Waiting for transfer job {job_id}."
1011
+ f"Waiting for job {job_id}."
943
1012
  )
944
1013
 
945
1014
  async def check_timeout(sleep_time):
@@ -950,8 +1019,7 @@ class AsyncFirecrest:
950
1019
  self.log(
951
1020
  logging.DEBUG,
952
1021
  f"Timeout is about to be reached, while waiting for "
953
- f"transfer job {job_id}. Will cancel the job to stop "
954
- f"the transfer."
1022
+ f"job {job_id}. Will cancel the job to stop the transfer."
955
1023
  )
956
1024
 
957
1025
  try:
@@ -962,7 +1030,7 @@ class AsyncFirecrest:
962
1030
  f"Failed to cancel job {job_id}: {e}"
963
1031
  )
964
1032
 
965
- raise TransferJobTimeoutException(job_info)
1033
+ raise JobTimeoutException(job_id)
966
1034
 
967
1035
  for i in sleep_generator():
968
1036
  try:
@@ -972,6 +1040,9 @@ class AsyncFirecrest:
972
1040
  e.responses[-1].status_code == 404 and
973
1041
  "Job not found" in e.responses[-1].json()['message']
974
1042
  ):
1043
+ if asyncio.get_event_loop().time() > not_found_timeout_time:
1044
+ raise e
1045
+
975
1046
  self.log(
976
1047
  logging.DEBUG,
977
1048
  f"Job {job_id} information is not yet available, will "
@@ -999,30 +1070,7 @@ class AsyncFirecrest:
999
1070
  await check_timeout(i)
1000
1071
  await asyncio.sleep(i)
1001
1072
 
1002
- try:
1003
- # If job is cancelled before it starts running, the log file will
1004
- # not be available
1005
- stdout_file = await self.view(
1006
- system_name,
1007
- job_info["transferJob"]["logs"]["outputLog"]
1008
- )
1009
- except FirecrestException as e:
1010
- if (
1011
- e.responses[-1].status_code == 404 and
1012
- "No such file or directory" in e.responses[-1].json()['message']
1013
- ):
1014
- raise TransferJobFailedException(
1015
- job_info, file_not_found=True
1016
- )
1017
- else:
1018
- raise e
1019
-
1020
- if (
1021
- "Files were successfully" not in stdout_file and
1022
- "File was successfully" not in stdout_file and
1023
- "Multipart file upload successfully completed" not in stdout_file
1024
- ):
1025
- raise TransferJobFailedException(job_info)
1073
+ return job
1026
1074
 
1027
1075
  async def cp(
1028
1076
  self,
@@ -1083,9 +1131,9 @@ class AsyncFirecrest:
1083
1131
  :param system_name: the system name where the filesystem belongs to
1084
1132
  :param path: the absolute target path
1085
1133
  :param account: the account to be used for the transfer job (only
1086
- relevant when the file is not deleted directly)
1134
+ relevant when the file is not deleted directly)
1087
1135
  :param blocking: whether to wait for the job to complete (only
1088
- relevant when the file is not deleted directly)
1136
+ relevant when the file is not deleted directly)
1089
1137
  :param timeout: the maximum time to wait for the job to complete
1090
1138
  :calls: DELETE `/filesystem/{system_name}/ops/rm`
1091
1139
 
@@ -25,6 +25,7 @@ from firecrest.utilities import (
25
25
  )
26
26
  from firecrest.FirecrestException import (
27
27
  FirecrestException,
28
+ JobTimeoutException,
28
29
  MultipartUploadException,
29
30
  TransferJobFailedException,
30
31
  TransferJobTimeoutException,
@@ -115,16 +116,16 @@ class ExternalUpload:
115
116
  else:
116
117
  content_length = chunk_size
117
118
 
118
- with open(self._local_file, "rb") as f:
119
- f.seek(start)
120
- resp = self._client._session.put(
121
- url=url,
122
- content=chunk_reader(f, self.chunk_size),
123
- timeout=None,
124
- headers={
125
- "Content-Length": str(content_length)
126
- }
127
- )
119
+ with open(self._local_file, "rb") as f:
120
+ f.seek(start)
121
+ resp = self._client._session.put(
122
+ url=url,
123
+ content=chunk_reader(f, self.chunk_size),
124
+ timeout=None,
125
+ headers={
126
+ "Content-Length": str(content_length)
127
+ }
128
+ )
128
129
 
129
130
  if resp.status_code >= 400:
130
131
  raise MultipartUploadException(
@@ -916,15 +917,83 @@ class Firecrest:
916
917
  )
917
918
  self._check_response(resp, 204)
918
919
 
919
- def _wait_for_transfer_job(self, job_info, timeout=None):
920
+ def _wait_for_transfer_job(
921
+ self,
922
+ job_info: dict,
923
+ timeout: Optional[float] = None
924
+ ) -> None:
925
+ job_id = job_info["transferJob"]["jobId"]
926
+ system_name = job_info["transferJob"]["system"]
927
+ try:
928
+ self.wait_for_job(
929
+ system_name,
930
+ job_id,
931
+ timeout=timeout
932
+ )
933
+ except JobTimeoutException:
934
+ self.log(
935
+ logging.DEBUG,
936
+ f"Transfer job {job_id} timed out"
937
+ )
938
+ raise TransferJobTimeoutException(job_info)
939
+
940
+ try:
941
+ # If job is cancelled before it starts running, the log file will
942
+ # not be available
943
+ stdout_file = self.view(
944
+ system_name,
945
+ job_info["transferJob"]["logs"]["outputLog"]
946
+ )
947
+ except FirecrestException as e:
948
+ if (
949
+ e.responses[-1].status_code == 404 and
950
+ "No such file or directory" in e.responses[-1].json()['message']
951
+ ):
952
+ raise TransferJobFailedException(
953
+ job_info, file_not_found=True
954
+ )
955
+ else:
956
+ raise e
957
+
958
+ if (
959
+ "Files were successfully" not in stdout_file and
960
+ "File was successfully" not in stdout_file and
961
+ "Multipart file upload successfully completed" not in stdout_file
962
+ ):
963
+ raise TransferJobFailedException(job_info)
964
+
965
+ def wait_for_job(
966
+ self,
967
+ system_name: str,
968
+ job_id: str,
969
+ timeout: Optional[float] = None,
970
+ not_found_timeout: float = 20.0
971
+ ) -> List[Any]:
972
+ """Wait for a job to complete. When the job is completed, it will
973
+ return the job information.
974
+ :param system_name: the system name where the filesystem belongs to
975
+ :param job_id: the ID of the job to wait for
976
+ :param timeout: the maximum time to wait for the job to complete
977
+ in seconds. If the timeout is set and the job is not
978
+ completed within this time, it will be cancelled.
979
+ :param not_found_timeout: the maximum time to wait for the job to be
980
+ available in the scheduler, in seconds.
981
+ Slurm or other scheduler will need a few
982
+ seconds before updating the database. If
983
+ the job is not found within this time, the
984
+ function will raise an exception.
985
+ :calls: GET `/jobs/{system_name}/{job_id}`
986
+ """
920
987
  if timeout:
921
988
  timeout_time = time.time() + timeout
922
989
 
923
- job_id = job_info["transferJob"]["jobId"]
924
- system_name = job_info["transferJob"]["system"]
990
+ not_found_timeout_time = (
991
+ time.time() + not_found_timeout
992
+ )
993
+
925
994
  self.log(
926
995
  logging.DEBUG,
927
- f"Waiting for transfer job {job_id}."
996
+ f"Waiting for job {job_id}."
928
997
  )
929
998
 
930
999
  def check_timeout(sleep_time):
@@ -935,8 +1004,7 @@ class Firecrest:
935
1004
  self.log(
936
1005
  logging.DEBUG,
937
1006
  f"Timeout is about to be reached, while waiting for "
938
- f"transfer job {job_id}. Will cancel the job to stop "
939
- f"the transfer."
1007
+ f"job {job_id}. Will cancel the job to stop the transfer."
940
1008
  )
941
1009
 
942
1010
  try:
@@ -947,7 +1015,7 @@ class Firecrest:
947
1015
  f"Failed to cancel job {job_id}: {e}"
948
1016
  )
949
1017
 
950
- raise TransferJobTimeoutException(job_info)
1018
+ raise JobTimeoutException(job_id)
951
1019
 
952
1020
  for i in sleep_generator():
953
1021
  try:
@@ -957,6 +1025,9 @@ class Firecrest:
957
1025
  e.responses[-1].status_code == 404 and
958
1026
  "Job not found" in e.responses[-1].json()['message']
959
1027
  ):
1028
+ if time.time() > not_found_timeout_time:
1029
+ raise e
1030
+
960
1031
  self.log(
961
1032
  logging.DEBUG,
962
1033
  f"Job {job_id} information is not yet available, will "
@@ -984,30 +1055,7 @@ class Firecrest:
984
1055
  check_timeout(i)
985
1056
  time.sleep(i)
986
1057
 
987
- try:
988
- # If job is cancelled before it starts running, the log file will
989
- # not be available
990
- stdout_file = self.view(
991
- system_name,
992
- job_info["transferJob"]["logs"]["outputLog"]
993
- )
994
- except FirecrestException as e:
995
- if (
996
- e.responses[-1].status_code == 404 and
997
- "No such file or directory" in e.responses[-1].json()['message']
998
- ):
999
- raise TransferJobFailedException(
1000
- job_info, file_not_found=True
1001
- )
1002
- else:
1003
- raise e
1004
-
1005
- if (
1006
- "Files were successfully" not in stdout_file and
1007
- "File was successfully" not in stdout_file and
1008
- "Multipart file upload successfully completed" not in stdout_file
1009
- ):
1010
- raise TransferJobFailedException(job_info)
1058
+ return job
1011
1059
 
1012
1060
  def cp(
1013
1061
  self,
@@ -1350,7 +1398,7 @@ class Firecrest:
1350
1398
  "be set."
1351
1399
  )
1352
1400
 
1353
- data: Any = {
1401
+ data: dict = {
1354
1402
  "job": {
1355
1403
  "working_directory": working_dir
1356
1404
  }
@@ -25,10 +25,10 @@ classifiers = [
25
25
  ]
26
26
  requires-python = ">=3.7"
27
27
  dependencies = [
28
- "aiofiles~=23.2.1",
28
+ "aiofiles>=23.2.1",
29
29
  "requests>=2.14.0",
30
30
  "PyJWT>=2.4.0",
31
- "typer[all]~=0.7.0",
31
+ "typer[all]>=0.7.0,<1.0.0",
32
32
  "packaging>=21.0",
33
33
  "httpx>=0.24.0",
34
34
  "PyYAML>=5.1"
@@ -1,47 +0,0 @@
1
- Authorization
2
- =============
3
-
4
- For every request to FirecREST you need to have a valid access token.
5
- This will enable your application to have access to the requested resources.
6
-
7
-
8
- .. Supported grant types
9
- .. ---------------------
10
-
11
- .. Implicit
12
- .. ^^^^^^^^
13
-
14
- .. Client Credentials
15
- .. ^^^^^^^^^^^^^^^^^^
16
-
17
- The pyFirecREST Authorization Object
18
- ------------------------------------
19
-
20
- You can take care of the access token by yourself any way you want, or even better use a python library to take care of this for you, depending on the grant type.
21
- What pyFirecREST will need in the end is only a python object with the method ``get_access_token()``, that when called will provide a valid access token.
22
-
23
- Let's say for example you have somehow obtained a long-lasting access token.
24
- The Authorization class you would need to make and give to Firecrest would look like this:
25
-
26
- .. code-block:: Python
27
-
28
- class MyAuthorizationClass:
29
- def __init__(self):
30
- pass
31
-
32
- def get_access_token(self):
33
- return <TOKEN>
34
-
35
- If this is your case you can move on to the next session, where you can see how to use this Authorization object, in order to make your requests.
36
-
37
- If you want to use the Client Credentials authorization grant, you can use the ``ClientCredentialsAuth`` class from pyFirecREST and setup the authorization object like this:
38
-
39
- .. code-block:: Python
40
-
41
- import firecrest as f7t
42
-
43
- keycloak = f7t.ClientCredentialsAuth(
44
- <client_id>, <client_secret>, <token_uri>
45
- )
46
-
47
- In a similar way you can reuse other packages to support different grant types, like `Flask-OIDC <https://flask-oidc.readthedocs.io/>`__.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes