sfapi-client 0.4.0__tar.gz → 0.5.0__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.
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/.github/workflows/black.yml +2 -2
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/PKG-INFO +2 -2
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/docs/quickstart/index.md +2 -4
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/pyproject.toml +1 -1
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/scripts/run.py +7 -3
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/__init__.py +1 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_async/client.py +15 -2
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_async/compute.py +110 -36
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_async/groups.py +2 -14
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_async/paths.py +38 -13
- sfapi_client-0.5.0/src/sfapi_client/_async/storage.py +210 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_async/users.py +6 -16
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_compute.py +7 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_models/__init__.py +31 -13
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_sync/client.py +15 -2
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_sync/compute.py +110 -35
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_sync/groups.py +2 -14
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_sync/paths.py +38 -13
- sfapi_client-0.5.0/src/sfapi_client/_sync/storage.py +210 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_sync/users.py +6 -16
- sfapi_client-0.5.0/src/sfapi_client/_utils.py +30 -0
- sfapi_client-0.5.0/src/sfapi_client/compute.py +8 -0
- sfapi_client-0.5.0/src/sfapi_client/storage.py +2 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/tests/conftest.py +4 -7
- sfapi_client-0.5.0/tests/test_compute.py +196 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/tests/test_compute_async.py +104 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/tests/test_paths_async.py +51 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/tests/test_resources.py +2 -6
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/tests/test_resources_async.py +2 -6
- sfapi_client-0.5.0/tests/test_storage.py +44 -0
- sfapi_client-0.5.0/tests/test_storage_async.py +46 -0
- sfapi_client-0.4.0/src/sfapi_client/_utils.py +0 -6
- sfapi_client-0.4.0/src/sfapi_client/compute.py +0 -3
- sfapi_client-0.4.0/tests/test_compute.py +0 -99
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/.github/dependabot.yml +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/.github/workflows/mkdocs.yml +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/.github/workflows/pypi.yml +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/.github/workflows/pytest.yml +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/.github/workflows/ruff.yml +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/.gitignore +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/.pre-commit-config.yaml +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/LEGAL +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/LICENSE +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/README.md +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/docs/community/contributing.md +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/docs/examples/check_current_and_past_jobs.ipynb +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/docs/examples/getting_bearer_token.ipynb +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/docs/examples/run_job_and_check_status.ipynb +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/docs/gen_examples.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/docs/gen_sync.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/docs/index.md +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/docs/javascript/apiselector.js +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/docs/reference/SUMMARY.txt +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/docs/reference/async/client/index.md +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/docs/reference/async/compute/index.md +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/docs/reference/async/exceptions/index.md +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/docs/reference/async/groups/index.md +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/docs/reference/async/jobs/index.md +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/docs/reference/async/paths/index.md +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/docs/reference/async/projects/index.md +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/docs/reference/async/resources/index.md +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/docs/reference/async/users/index.md +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/mkdocs.yml +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/pytest.ini +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/requirements-dev.txt +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_async/__init__.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_async/jobs.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_async/projects.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_jobs.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_models/job_status_response_sacct.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_models/job_status_response_squeue.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_models/resources.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_monitor.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_sync/__init__.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_sync/jobs.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/_sync/projects.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/client.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/exceptions.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/groups.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/jobs.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/paths.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/projects.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/resources.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/src/sfapi_client/users.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/tests/test_api.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/tests/test_api_async.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/tests/test_client.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/tests/test_client_async.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/tests/test_groups.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/tests/test_groups_async.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/tests/test_jobs.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/tests/test_jobs_async.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/tests/test_key.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/tests/test_paths.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/tests/test_projects.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/tests/test_projects_async.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/tests/test_users.py +0 -0
- {sfapi_client-0.4.0 → sfapi_client-0.5.0}/tests/test_users_async.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sfapi_client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Python client for NERSC SF API
|
|
5
5
|
Author-email: Chris Harris <cjh@lbl.gov>, Nicholas Tyler <tylern@lbl.gov>
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Classifier: License :: OSI Approved :: BSD License
|
|
8
8
|
Classifier: Operating System :: OS Independent
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.8
|
|
11
11
|
Requires-Dist: authlib
|
|
12
12
|
Requires-Dist: httpx
|
|
13
13
|
Requires-Dist: pydantic-settings
|
|
@@ -95,13 +95,12 @@ export SFAPI_SECRET='{"kty": "RSA", "n": ...}'
|
|
|
95
95
|
```pycon
|
|
96
96
|
>>> from sfapi_client import AsyncClient
|
|
97
97
|
>>> from sfapi_client.compute import Machine
|
|
98
|
-
>>> from authlib.jose import JsonWebKey
|
|
99
98
|
>>> import json
|
|
100
99
|
>>> import os
|
|
101
100
|
>>>
|
|
102
101
|
>>> client_id = os.getenv("SFAPI_CLIENT_ID")
|
|
103
102
|
>>> sfapi_secret = os.getenv("SFAPI_SECRET")
|
|
104
|
-
>>> client_secret =
|
|
103
|
+
>>> client_secret = json.loads(sfapi_secret)
|
|
105
104
|
>>>
|
|
106
105
|
>>> async with AsyncClient(client_id, client_secret) as client:
|
|
107
106
|
... perlmutter = await client.compute(Machine.perlmutter)
|
|
@@ -110,13 +109,12 @@ export SFAPI_SECRET='{"kty": "RSA", "n": ...}'
|
|
|
110
109
|
```pycon
|
|
111
110
|
>>> from sfapi_client import Client
|
|
112
111
|
>>> from sfapi_client.compute import Machine
|
|
113
|
-
>>> from authlib.jose import JsonWebKey
|
|
114
112
|
>>> import json
|
|
115
113
|
>>> import os
|
|
116
114
|
>>>
|
|
117
115
|
>>> client_id = os.getenv("SFAPI_CLIENT_ID")
|
|
118
116
|
>>> sfapi_secret = os.getenv("SFAPI_SECRET")
|
|
119
|
-
>>> client_secret =
|
|
117
|
+
>>> client_secret = json.loads(sfapi_secret)
|
|
120
118
|
>>>
|
|
121
119
|
>>> with Client(client_id, client_secret) as client:
|
|
122
120
|
... perlmutter = client.compute(Machine.perlmutter)
|
|
@@ -10,7 +10,6 @@ from typing import List, Optional
|
|
|
10
10
|
|
|
11
11
|
from datamodel_code_generator import InputFileType, generate
|
|
12
12
|
|
|
13
|
-
|
|
14
13
|
cli = typer.Typer()
|
|
15
14
|
|
|
16
15
|
|
|
@@ -24,6 +23,8 @@ def run_unasync():
|
|
|
24
23
|
"AsyncApi": "Api",
|
|
25
24
|
"AsyncResources": "Resources",
|
|
26
25
|
"AsyncCompute": "Compute",
|
|
26
|
+
"AsyncCommandTask": "CommandTask",
|
|
27
|
+
"AsyncCommandTaskResult": "CommandTaskResult",
|
|
27
28
|
"AsyncGroup": "Group",
|
|
28
29
|
"AsyncGroupMember": "GroupMember",
|
|
29
30
|
"AsyncRemotePath": "RemotePath",
|
|
@@ -34,6 +35,9 @@ def run_unasync():
|
|
|
34
35
|
"AsyncJobSacct": "JobSacct",
|
|
35
36
|
"AsyncJobSqueue": "JobSqueue",
|
|
36
37
|
"AsyncOAuth2Client": "OAuth2Client",
|
|
38
|
+
"AsyncStorage": "Storage",
|
|
39
|
+
"AsyncGlobusStorage": "GlobusStorage",
|
|
40
|
+
"AsyncGlobusTransfer": "GlobusTransfer",
|
|
37
41
|
"aclose": "close",
|
|
38
42
|
"_ASYNC_SLEEP": "_SLEEP",
|
|
39
43
|
}
|
|
@@ -100,7 +104,7 @@ def _to_str_enum(code: str) -> str:
|
|
|
100
104
|
def _fix_date_import(code: str) -> str:
|
|
101
105
|
replacements = {
|
|
102
106
|
"import date": "import date as date_",
|
|
103
|
-
"date: Optional
|
|
107
|
+
"date: Optional\\[date\\]": "date: Optional[date_]",
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
for target, replacement in replacements.items():
|
|
@@ -193,7 +197,7 @@ def resources_codegen(
|
|
|
193
197
|
name = name if name != "int" else f"_{name}"
|
|
194
198
|
name = name.replace("-", "_")
|
|
195
199
|
names.append(f' {name} = "{value}"')
|
|
196
|
-
names.append(f
|
|
200
|
+
names.append(f' """ {s["system_type"]}: {s["full_name"]}"""')
|
|
197
201
|
|
|
198
202
|
resources = "\n".join(names)
|
|
199
203
|
now = datetime.datetime.now()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from .client import AsyncClient # noqa: F401
|
|
2
2
|
from .client import Client # noqa: F401
|
|
3
3
|
from .exceptions import SfApiError # noqa: F401
|
|
4
|
+
from ._models import OutageStatusValue # noqa: F401
|
|
4
5
|
from ._models import StatusValue # noqa: F401
|
|
5
6
|
from ._models.resources import Resource # noqa: F401
|
|
@@ -7,7 +7,6 @@ from authlib.integrations.httpx_client.oauth2_client import AsyncOAuth2Client
|
|
|
7
7
|
from authlib.oauth2.rfc7523 import PrivateKeyJWT
|
|
8
8
|
import httpx
|
|
9
9
|
import tenacity
|
|
10
|
-
from authlib.jose import JsonWebKey
|
|
11
10
|
|
|
12
11
|
from .compute import Machine, AsyncCompute
|
|
13
12
|
from ..exceptions import ClientKeyError
|
|
@@ -21,6 +20,7 @@ from .._models import (
|
|
|
21
20
|
from .._models.resources import Resource
|
|
22
21
|
from .groups import AsyncGroup, AsyncGroupMember
|
|
23
22
|
from .users import AsyncUser
|
|
23
|
+
from .storage import AsyncGlobusStorage, AsyncStorage
|
|
24
24
|
from .projects import AsyncProject, AsyncRole
|
|
25
25
|
from .paths import AsyncRemotePath
|
|
26
26
|
|
|
@@ -252,6 +252,7 @@ class AsyncClient:
|
|
|
252
252
|
self.__http_client = None
|
|
253
253
|
self._api = None
|
|
254
254
|
self._resources = None
|
|
255
|
+
self._storage = None
|
|
255
256
|
self._wait_interval = wait_interval
|
|
256
257
|
self._access_token = access_token
|
|
257
258
|
|
|
@@ -350,7 +351,7 @@ class AsyncClient:
|
|
|
350
351
|
if key_path.suffix == ".json":
|
|
351
352
|
# Json file in the format {"client_id": "", "secret": ""}
|
|
352
353
|
json_web_key = json.loads(secret.read())
|
|
353
|
-
self._secret =
|
|
354
|
+
self._secret = json_web_key["secret"]
|
|
354
355
|
self._client_id = json_web_key["client_id"]
|
|
355
356
|
else:
|
|
356
357
|
self._secret = secret.read()
|
|
@@ -504,8 +505,19 @@ class AsyncClient:
|
|
|
504
505
|
|
|
505
506
|
return self._resources
|
|
506
507
|
|
|
508
|
+
@property
|
|
509
|
+
def storage(self) -> AsyncStorage:
|
|
510
|
+
"""Storage related objects and methods
|
|
511
|
+
|
|
512
|
+
:return AsyncStorage: Object with storage related objects and methods
|
|
513
|
+
"""
|
|
514
|
+
if self._storage is None:
|
|
515
|
+
self._storage = AsyncStorage(self)
|
|
516
|
+
return self._storage
|
|
517
|
+
|
|
507
518
|
|
|
508
519
|
AsyncCompute.model_rebuild()
|
|
520
|
+
AsyncGlobusStorage.model_rebuild()
|
|
509
521
|
AsyncGroup.model_rebuild()
|
|
510
522
|
AsyncUser.model_rebuild()
|
|
511
523
|
AsyncProject.model_rebuild()
|
|
@@ -513,6 +525,7 @@ AsyncRemotePath.model_rebuild()
|
|
|
513
525
|
AsyncRole.model_rebuild()
|
|
514
526
|
AsyncGroupMember.model_rebuild()
|
|
515
527
|
|
|
528
|
+
|
|
516
529
|
# Ensure that the job models are built, we need to import here to
|
|
517
530
|
# avoid circular imports
|
|
518
531
|
from .jobs import AsyncJobSacct, AsyncJobSqueue # noqa: E402
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
from typing import Dict, List, Optional, Union
|
|
2
|
-
import
|
|
3
|
-
from pydantic import PrivateAttr, ConfigDict
|
|
1
|
+
from typing import Dict, List, Optional, Union
|
|
2
|
+
from pydantic import PrivateAttr, ConfigDict, BaseModel
|
|
4
3
|
from ..exceptions import SfApiError
|
|
5
4
|
from .._utils import _ASYNC_SLEEP
|
|
6
5
|
from .jobs import AsyncJobSacct, AsyncJobSqueue, JobCommand
|
|
7
6
|
from .._models import (
|
|
8
7
|
AppRoutersStatusModelsStatus as ComputeBase,
|
|
9
|
-
Task,
|
|
10
|
-
StatusValue,
|
|
8
|
+
Task as TaskResponse,
|
|
11
9
|
PublicHost as Machine,
|
|
12
10
|
BodyRunCommandUtilitiesCommandMachinePost as RunCommandBody,
|
|
13
11
|
AppRoutersComputeModelsCommandOutput as RunCommandResponse,
|
|
@@ -15,25 +13,85 @@ from .._models import (
|
|
|
15
13
|
)
|
|
16
14
|
from .paths import AsyncRemotePath
|
|
17
15
|
from .._monitor import AsyncJobMonitor
|
|
18
|
-
from .._compute import
|
|
16
|
+
from .._compute import SubmitJobResponse, SubmitJobResponseStatus, TaskStatus
|
|
17
|
+
from .._utils import check_auth
|
|
19
18
|
|
|
20
19
|
# Patch to return str names from Enum of py3.11
|
|
21
20
|
Machine.__str__ = lambda self: self.value
|
|
22
21
|
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
23
|
+
class AsyncCommandTaskResult(BaseModel):
|
|
24
|
+
"""
|
|
25
|
+
Result data returned by a command execution task.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
model_config = ConfigDict(extra="allow")
|
|
29
|
+
|
|
30
|
+
status: Optional[str] = None
|
|
31
|
+
output: Optional[str] = None
|
|
32
|
+
error: Optional[str] = None
|
|
33
|
+
exit_code: Optional[int] = None
|
|
34
|
+
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
class AsyncCommandTask(BaseModel):
|
|
37
|
+
"""
|
|
38
|
+
Models an asynchronous command execution task.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
compute: "AsyncCompute"
|
|
42
|
+
id: str
|
|
43
|
+
status: Optional[TaskStatus] = None
|
|
44
|
+
result: Optional[AsyncCommandTaskResult] = None
|
|
45
|
+
|
|
46
|
+
async def _fetch(self) -> TaskResponse:
|
|
47
|
+
"""
|
|
48
|
+
Fetch the latest task state from the API.
|
|
49
|
+
|
|
50
|
+
:return: The raw task response returned by the API.
|
|
51
|
+
"""
|
|
52
|
+
r = await self.compute.client.get(f"tasks/{self.id}")
|
|
53
|
+
json_response = r.json()
|
|
54
|
+
|
|
55
|
+
return TaskResponse.model_validate(json_response)
|
|
56
|
+
|
|
57
|
+
async def update(self):
|
|
58
|
+
"""
|
|
59
|
+
Refresh the task state.
|
|
60
|
+
|
|
61
|
+
:return: The updated task instance.
|
|
62
|
+
"""
|
|
63
|
+
task = await self._fetch()
|
|
64
|
+
if task.status is not None:
|
|
65
|
+
# Map the status return by the API (lowercase string) to our TaskStatus enum ( uppercase )
|
|
66
|
+
# We do this for consistency as all our enums are uppercase
|
|
67
|
+
self.status = TaskStatus[task.status.upper()]
|
|
68
|
+
|
|
69
|
+
if task.result is not None:
|
|
70
|
+
self.result = AsyncCommandTaskResult.model_validate_json(task.result)
|
|
71
|
+
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
async def _wait(self):
|
|
75
|
+
while True:
|
|
76
|
+
await self.update()
|
|
77
|
+
|
|
78
|
+
if self.status in [
|
|
79
|
+
TaskStatus.COMPLETED,
|
|
80
|
+
TaskStatus.CANCELLED,
|
|
81
|
+
TaskStatus.FAILED,
|
|
82
|
+
]:
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
await _ASYNC_SLEEP(1)
|
|
86
|
+
|
|
87
|
+
def __await__(self):
|
|
88
|
+
return self._wait().__await__()
|
|
89
|
+
|
|
90
|
+
async def cancel(self):
|
|
91
|
+
"""
|
|
92
|
+
Cancel the running task.
|
|
93
|
+
"""
|
|
94
|
+
await self.compute.client.delete(f"tasks/{self.id}")
|
|
37
95
|
|
|
38
96
|
|
|
39
97
|
class AsyncCompute(ComputeBase):
|
|
@@ -51,18 +109,17 @@ class AsyncCompute(ComputeBase):
|
|
|
51
109
|
kwargs["exclude"] = {"client"}
|
|
52
110
|
return super().dict(*args, **kwargs)
|
|
53
111
|
|
|
54
|
-
async def _wait_for_task(self, task_id) ->
|
|
55
|
-
|
|
56
|
-
|
|
112
|
+
async def _wait_for_task(self, task_id) -> AsyncCommandTaskResult:
|
|
113
|
+
|
|
114
|
+
task = AsyncCommandTask(compute=self, id=task_id)
|
|
57
115
|
|
|
58
|
-
|
|
59
|
-
task
|
|
116
|
+
while True:
|
|
117
|
+
await task.update()
|
|
60
118
|
|
|
61
|
-
|
|
62
|
-
if status in ["error", "failed"]:
|
|
119
|
+
if task.status is TaskStatus.FAILED:
|
|
63
120
|
raise SfApiError(task.result)
|
|
64
121
|
|
|
65
|
-
if status not in [
|
|
122
|
+
if task.status not in [TaskStatus.COMPLETED, TaskStatus.CANCELLED]:
|
|
66
123
|
await _ASYNC_SLEEP(1)
|
|
67
124
|
continue
|
|
68
125
|
|
|
@@ -107,11 +164,11 @@ class AsyncCompute(ComputeBase):
|
|
|
107
164
|
|
|
108
165
|
# We now need waiting for the task to complete!
|
|
109
166
|
task_result = await self._wait_for_task(task_id)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
raise SfApiError(result["error"])
|
|
167
|
+
if task_result.status == "error":
|
|
168
|
+
raise SfApiError(task_result.error)
|
|
113
169
|
|
|
114
|
-
jobid
|
|
170
|
+
# Get the jobid from the extra fields of the result
|
|
171
|
+
jobid = task_result.__pydantic_extra__.get("jobid")
|
|
115
172
|
if jobid is None:
|
|
116
173
|
raise SfApiError(f"Unable to extract jobid if for task: {task_id}")
|
|
117
174
|
|
|
@@ -155,6 +212,17 @@ class AsyncCompute(ComputeBase):
|
|
|
155
212
|
|
|
156
213
|
@check_auth
|
|
157
214
|
async def run(self, args: Union[str, AsyncRemotePath, List[str]]):
|
|
215
|
+
task = await self._run_task(args)
|
|
216
|
+
await task._wait()
|
|
217
|
+
|
|
218
|
+
if task.result.error:
|
|
219
|
+
raise SfApiError(task.result.error)
|
|
220
|
+
|
|
221
|
+
return task.result.output
|
|
222
|
+
|
|
223
|
+
async def _run_task(
|
|
224
|
+
self, args: Union[str, AsyncRemotePath, List[str]]
|
|
225
|
+
) -> AsyncCommandTask:
|
|
158
226
|
body: RunCommandBody = {
|
|
159
227
|
"executable": args if not isinstance(args, list) else " ".join(args)
|
|
160
228
|
}
|
|
@@ -165,13 +233,19 @@ class AsyncCompute(ComputeBase):
|
|
|
165
233
|
if run_response.status == RunCommandResponseStatus.ERROR:
|
|
166
234
|
raise SfApiError(run_response.error)
|
|
167
235
|
|
|
168
|
-
|
|
169
|
-
task_result = await self._wait_for_task(task_id)
|
|
170
|
-
command_result = CommandResult.model_validate_json(task_result)
|
|
171
|
-
if command_result.status == "error":
|
|
172
|
-
raise SfApiError(command_result.error)
|
|
236
|
+
return AsyncCommandTask(compute=self, id=run_response.task_id)
|
|
173
237
|
|
|
174
|
-
|
|
238
|
+
@check_auth
|
|
239
|
+
async def run_task(
|
|
240
|
+
self, args: Union[str, AsyncRemotePath, List[str]]
|
|
241
|
+
) -> AsyncCommandTask:
|
|
242
|
+
"""
|
|
243
|
+
Submit a command to the compute resource and return a task.
|
|
244
|
+
|
|
245
|
+
:param args: Command to execute.
|
|
246
|
+
:return: Task object that can be awaited ( async version ), updated, or cancelled.
|
|
247
|
+
"""
|
|
248
|
+
return await self._run_task(args)
|
|
175
249
|
|
|
176
250
|
async def outages(self):
|
|
177
251
|
return await self.client.resources.outages(self.name)
|
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
from typing import Optional, Union, List, Any
|
|
2
|
-
from functools import wraps
|
|
1
|
+
from typing import Optional, Union, List, Any
|
|
3
2
|
from pydantic import ValidationError, Field, BaseModel, ConfigDict
|
|
4
3
|
from .._models import BatchGroupAction as GroupAction, UserStats as GroupMemberBase
|
|
5
4
|
from ..exceptions import SfApiError
|
|
6
5
|
from .users import AsyncUser
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def check_auth(method: Callable):
|
|
10
|
-
@wraps(method)
|
|
11
|
-
def wrapper(self, *args, **kwargs):
|
|
12
|
-
if self._client_id is None:
|
|
13
|
-
raise SfApiError(
|
|
14
|
-
f"Cannot call {self.__class__.__name__}.{method.__name__}() with an unauthenticated client." # noqa: E501
|
|
15
|
-
)
|
|
16
|
-
return method(self, *args, **kwargs)
|
|
17
|
-
|
|
18
|
-
return wrapper
|
|
6
|
+
from .._utils import check_auth
|
|
19
7
|
|
|
20
8
|
|
|
21
9
|
class AsyncGroupMember(GroupMemberBase):
|
|
@@ -5,6 +5,7 @@ from io import StringIO, BytesIO
|
|
|
5
5
|
from base64 import b64decode
|
|
6
6
|
from contextlib import asynccontextmanager
|
|
7
7
|
import tempfile
|
|
8
|
+
import re
|
|
8
9
|
|
|
9
10
|
from .._models import (
|
|
10
11
|
DirectoryEntry as PathBase,
|
|
@@ -22,6 +23,9 @@ def _is_no_such(error: SfApiError):
|
|
|
22
23
|
return "No such file or directory" in error.message
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
SYMLINK_REGEX = r"^(\/.*) ->"
|
|
27
|
+
|
|
28
|
+
|
|
25
29
|
class AsyncRemotePath(PathBase):
|
|
26
30
|
"""
|
|
27
31
|
RemotePath is used to model a remote path, it takes inspiration from
|
|
@@ -133,7 +137,16 @@ class AsyncRemotePath(PathBase):
|
|
|
133
137
|
"""
|
|
134
138
|
:return: Returns True if path is a file, False otherwise.
|
|
135
139
|
"""
|
|
136
|
-
|
|
140
|
+
if self.perms is None:
|
|
141
|
+
await self.update()
|
|
142
|
+
|
|
143
|
+
return self.perms[0] == "-"
|
|
144
|
+
|
|
145
|
+
async def is_symlink(self) -> bool:
|
|
146
|
+
if self.perms is None:
|
|
147
|
+
await self.update()
|
|
148
|
+
|
|
149
|
+
return self.perms[0] == "l"
|
|
137
150
|
|
|
138
151
|
async def download(self, binary=False) -> IO[AnyStr]:
|
|
139
152
|
"""
|
|
@@ -148,7 +161,8 @@ class AsyncRemotePath(PathBase):
|
|
|
148
161
|
raise IsADirectoryError(self._path)
|
|
149
162
|
|
|
150
163
|
r = await self.compute.client.get(
|
|
151
|
-
f"utilities/download/{self.compute.name}/{self._path}
|
|
164
|
+
f"utilities/download/{self.compute.name}/{self._path}",
|
|
165
|
+
params={"binary": binary},
|
|
152
166
|
)
|
|
153
167
|
json_response = r.json()
|
|
154
168
|
download_response = FileDownloadResponse.model_validate(json_response)
|
|
@@ -188,15 +202,22 @@ class AsyncRemotePath(PathBase):
|
|
|
188
202
|
|
|
189
203
|
return p
|
|
190
204
|
|
|
191
|
-
# Special case for listing file
|
|
205
|
+
# Special case for listing file or symlink
|
|
192
206
|
if len(directory_listing_response.entries) == 1:
|
|
193
207
|
entry = directory_listing_response.entries[0]
|
|
194
|
-
#
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
208
|
+
# symlink
|
|
209
|
+
if entry.perms[0] == "l":
|
|
210
|
+
if match := re.search(SYMLINK_REGEX, entry.name):
|
|
211
|
+
entry.name = match.group(1)
|
|
212
|
+
# file
|
|
213
|
+
else:
|
|
214
|
+
# The API can add an extra /
|
|
215
|
+
path = entry.name
|
|
216
|
+
if entry.name.startswith("//"):
|
|
217
|
+
path = path[1:]
|
|
218
|
+
filename = PurePosixPath(path).name
|
|
219
|
+
entry.name = filename
|
|
220
|
+
|
|
200
221
|
paths.append(_to_remote_path(path, entry))
|
|
201
222
|
else:
|
|
202
223
|
for entry in directory_listing_response.entries:
|
|
@@ -209,6 +230,10 @@ class AsyncRemotePath(PathBase):
|
|
|
209
230
|
elif directory and entry.name == ".":
|
|
210
231
|
entry.name = PurePosixPath(path).name
|
|
211
232
|
return [_to_remote_path(path, entry)]
|
|
233
|
+
# Special case for symlink
|
|
234
|
+
elif entry.perms[0] == "l":
|
|
235
|
+
if match := re.search(SYMLINK_REGEX, entry.name):
|
|
236
|
+
entry.name = match.group(1)
|
|
212
237
|
|
|
213
238
|
paths.append(_to_remote_path(f"{path}/{entry.name}", entry))
|
|
214
239
|
|
|
@@ -260,9 +285,9 @@ class AsyncRemotePath(PathBase):
|
|
|
260
285
|
if not _is_no_such(ex):
|
|
261
286
|
raise
|
|
262
287
|
|
|
263
|
-
# Check if the parent is a directory ( as in we are creating a new file ),
|
|
288
|
+
# Check if the parent is a directory or symlink ( as in we are creating a new file ),
|
|
264
289
|
# if not re raise the original exception
|
|
265
|
-
if not await self.parent.is_dir():
|
|
290
|
+
if not (await self.parent.is_dir() or await self.parent.is_symlink()):
|
|
266
291
|
raise
|
|
267
292
|
else:
|
|
268
293
|
upload_path = str(self._path)
|
|
@@ -301,9 +326,9 @@ class AsyncRemotePath(PathBase):
|
|
|
301
326
|
if not _is_no_such(ex):
|
|
302
327
|
raise
|
|
303
328
|
|
|
304
|
-
# Check if the parent is a directory ( as in we are creating a new file ),
|
|
329
|
+
# Check if the parent is a directory or symlink ( as in we are creating a new file ),
|
|
305
330
|
# if not re raise the original exception
|
|
306
|
-
if not await self.parent.is_dir():
|
|
331
|
+
if not (await self.parent.is_dir() or await self.parent.is_symlink()):
|
|
307
332
|
raise
|
|
308
333
|
|
|
309
334
|
valid_modes_chars = set("rwb")
|