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.
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/PKG-INFO +3 -3
- pyfirecrest-3.3.1/docs/source/authorization.rst +121 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/FirecrestException.py +12 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/__init__.py +28 -14
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/cli2/__init__.py +19 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/v1/__init__.py +12 -1
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/v2/__init__.py +9 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/v2/_async/Client.py +81 -33
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/v2/_sync/Client.py +90 -42
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/pyproject.toml +2 -2
- pyfirecrest-3.2.2/docs/source/authorization.rst +0 -47
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/.github/workflows/python-publish.yml +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/.github/workflows/testing.yml +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/.gitignore +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/.mypy.ini +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/.readthedocs.yml +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/LICENSE +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/README.md +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/async-example.md +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/Makefile +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/conf.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/index.rst +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/reference_async.rst +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/reference_async_v2.rst +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/reference_auth.rst +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/reference_basic.rst +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/reference_cli.rst +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/reference_sync_v2.rst +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/reference_v1_index.rst +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/reference_v2_index.rst +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/tutorial_async.rst +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/tutorial_basic_v1.rst +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/tutorial_basic_v2.rst +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/tutorial_cli.rst +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/tutorial_errors.rst +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/tutorial_index.rst +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/docs/source/tutorial_logging.rst +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/Authorization.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/cli/__init__.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/cli_script.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/path.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/py.typed +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/types.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/utilities.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/v1/AsyncClient.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/v1/AsyncExternalStorage.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/v1/BasicClient.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/v1/ExternalStorage.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/firecrest/v2/_async/__init__.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/common.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/context.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_authorisation.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_compute.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_compute_async.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_extras.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_extras_async.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_status.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_status_async.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_storage.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_storage_async.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_utilities.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/test_utilities_async.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/common.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/context_v2.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/handlers.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/checksum.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/chmod.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/chown.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/chown_not_permitted.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/compress.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/delete.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/extract.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/file.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/head.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/head_bytes.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/head_bytes_exclude_trailing.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/head_lines.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/head_lines_exclude_trailing.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/job_info.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/job_metadata.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/job_submit.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls_dereference.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls_hidden.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls_home.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls_home_dereference.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls_home_hidden.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls_home_recursive.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls_home_uid.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls_invalid_path.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls_recursive.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/ls_uid.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/mkdir.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/nodes.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/partitions.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/reservations.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/rm.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/stat.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/stat_dereference.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/symlink.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/systems.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/tail.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/tail_bytes.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/tail_bytes_exclude_beginning.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/tail_lines.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/tail_lines_exclude_beginning.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/userinfo.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/view.json +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/test_v2_async.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/test_v2_cli.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/test_v2_sync.py +0 -0
- {pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tox.ini +0 -0
- {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.
|
|
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
|
|
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]
|
|
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
|
+
]
|
|
@@ -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(
|
|
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
|
-
|
|
939
|
-
|
|
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
|
|
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"
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1134
|
+
relevant when the file is not deleted directly)
|
|
1087
1135
|
:param blocking: whether to wait for the job to complete (only
|
|
1088
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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(
|
|
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
|
-
|
|
924
|
-
|
|
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
|
|
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"
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
28
|
+
"aiofiles>=23.2.1",
|
|
29
29
|
"requests>=2.14.0",
|
|
30
30
|
"PyJWT>=2.4.0",
|
|
31
|
-
"typer[all]
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/tail_bytes_exclude_beginning.json
RENAMED
|
File without changes
|
|
File without changes
|
{pyfirecrest-3.2.2 → pyfirecrest-3.3.1}/tests/v2/responses/tail_lines_exclude_beginning.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|