qbraid-core 0.1.0.dev0__tar.gz → 0.1.0.dev1__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.
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/.gitignore +1 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/PKG-INFO +5 -4
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/README.md +4 -3
- qbraid-core-0.1.0.dev1/docs/api/qbraid_core.services.rst +16 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/index.rst +1 -3
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/pyproject.toml +1 -1
- qbraid-core-0.1.0.dev1/qbraid_core/__init__.py +106 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core/_version.py +2 -2
- qbraid-core-0.1.0.dev1/qbraid_core/client.py +110 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core/exceptions.py +8 -0
- qbraid-core-0.1.0.dev1/qbraid_core/registry.py +77 -0
- qbraid-core-0.1.0.dev1/qbraid_core/retry.py +83 -0
- {qbraid-core-0.1.0.dev0/qbraid_core → qbraid-core-0.1.0.dev1/qbraid_core/services}/environments/__init__.py +18 -8
- qbraid-core-0.1.0.dev1/qbraid_core/services/environments/client.py +105 -0
- {qbraid-core-0.1.0.dev0/qbraid_core → qbraid-core-0.1.0.dev1/qbraid_core/services}/environments/create.py +1 -1
- qbraid-core-0.1.0.dev1/qbraid_core/services/environments/exceptions.py +18 -0
- {qbraid-core-0.1.0.dev0/qbraid_core → qbraid-core-0.1.0.dev1/qbraid_core/services}/environments/paths.py +38 -7
- qbraid-core-0.1.0.dev1/qbraid_core/services/quantum/__init__.py +41 -0
- qbraid-core-0.1.0.dev1/qbraid_core/services/quantum/adapter.py +120 -0
- qbraid-core-0.1.0.dev1/qbraid_core/services/quantum/client.py +146 -0
- qbraid-core-0.1.0.dev1/qbraid_core/services/quantum/exceptions.py +17 -0
- qbraid-core-0.1.0.dev0/qbraid_core/jobs/state.py → qbraid-core-0.1.0.dev1/qbraid_core/services/quantum/proxy.py +33 -10
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core/session.py +63 -133
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core.egg-info/PKG-INFO +5 -4
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core.egg-info/SOURCES.txt +18 -16
- qbraid-core-0.1.0.dev1/tests/fixtures/__init__.py +0 -0
- qbraid-core-0.1.0.dev1/tests/test_client.py +48 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/tests/test_environments.py +8 -4
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/tests/test_session.py +2 -16
- qbraid-core-0.1.0.dev0/docs/api/qbraid_core.devices.rst +0 -7
- qbraid-core-0.1.0.dev0/docs/api/qbraid_core.environments.rst +0 -7
- qbraid-core-0.1.0.dev0/docs/api/qbraid_core.jobs.rst +0 -7
- qbraid-core-0.1.0.dev0/qbraid_core/__init__.py +0 -30
- qbraid-core-0.1.0.dev0/qbraid_core/devices/__init__.py +0 -27
- qbraid-core-0.1.0.dev0/qbraid_core/devices/data.py +0 -76
- qbraid-core-0.1.0.dev0/qbraid_core/devices/exceptions.py +0 -13
- qbraid-core-0.1.0.dev0/qbraid_core/environments/exceptions.py +0 -13
- qbraid-core-0.1.0.dev0/qbraid_core/jobs/__init__.py +0 -29
- qbraid-core-0.1.0.dev0/qbraid_core/jobs/data.py +0 -48
- qbraid-core-0.1.0.dev0/qbraid_core/jobs/exceptions.py +0 -13
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/.github/workflows/docs.yml +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/.github/workflows/format.yml +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/.github/workflows/main.yml +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/.github/workflows/publish.yml +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/.github/workflows/test-publish.yml +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/.readthedocs.yaml +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/LICENSE +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/MANIFEST.in +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/Makefile +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/_static/cards/jupyter.png +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/_static/cards/python.png +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/_static/cards/terminal.png +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/_static/css/custom.css +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/_static/css/s4defs-roles.css +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/_static/favicon.ico +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/_static/logo.png +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/api/qbraid_core.rst +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/api/qbraid_core.system.rst +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/conf.py +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/make.bat +0 -0
- {qbraid-core-0.1.0.dev0/tests → qbraid-core-0.1.0.dev1/qbraid_core/services}/__init__.py +0 -0
- {qbraid-core-0.1.0.dev0/qbraid_core → qbraid-core-0.1.0.dev1/qbraid_core/services}/environments/state.py +0 -0
- {qbraid-core-0.1.0.dev0/qbraid_core → qbraid-core-0.1.0.dev1/qbraid_core/services}/environments/validate.py +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core/system/__init__.py +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core/system/exceptions.py +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core/system/executables.py +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core/system/generic.py +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core/system/packages.py +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core/system/threader.py +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core.egg-info/dependency_links.txt +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core.egg-info/requires.txt +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core.egg-info/top_level.txt +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/setup.cfg +0 -0
- {qbraid-core-0.1.0.dev0/tests/fixtures → qbraid-core-0.1.0.dev1/tests}/__init__.py +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/tests/conftest.py +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/tests/fixtures/environments.py +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/tests/test_system.py +0 -0
- {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/tools/verify_headers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: qbraid-core
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev1
|
|
4
4
|
Summary: Python library with core abstractions for software development in the qBraid ecosystem.
|
|
5
5
|
Author-email: qBraid Development Team <contact@qbraid.com>
|
|
6
6
|
License: Proprietary
|
|
@@ -88,10 +88,11 @@ Other credential configuration methods can be found [here](https://docs.qbraid.c
|
|
|
88
88
|
After configuring your qBraid credentials, verify your setup by running the following from a Python interpreter:
|
|
89
89
|
|
|
90
90
|
```python
|
|
91
|
-
>>>
|
|
92
|
-
>>>
|
|
91
|
+
>>> import qbraid_core
|
|
92
|
+
>>> quantum_client = qbraid_core.client('quantum')
|
|
93
|
+
>>> device_data = quantum_client.search_devices()
|
|
93
94
|
>>> for item in device_data:
|
|
94
|
-
... print(item)
|
|
95
|
+
... print(item['qbraid_id'])
|
|
95
96
|
```
|
|
96
97
|
|
|
97
98
|
## Community
|
|
@@ -50,10 +50,11 @@ Other credential configuration methods can be found [here](https://docs.qbraid.c
|
|
|
50
50
|
After configuring your qBraid credentials, verify your setup by running the following from a Python interpreter:
|
|
51
51
|
|
|
52
52
|
```python
|
|
53
|
-
>>>
|
|
54
|
-
>>>
|
|
53
|
+
>>> import qbraid_core
|
|
54
|
+
>>> quantum_client = qbraid_core.client('quantum')
|
|
55
|
+
>>> device_data = quantum_client.search_devices()
|
|
55
56
|
>>> for item in device_data:
|
|
56
|
-
... print(item)
|
|
57
|
+
... print(item['qbraid_id'])
|
|
57
58
|
```
|
|
58
59
|
|
|
59
60
|
## Community
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Copyright (c) 2024, qBraid Development Team
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
This top level module contains the main qBraid public functionality.
|
|
6
|
+
|
|
7
|
+
.. currentmodule:: qbraid_core
|
|
8
|
+
|
|
9
|
+
Classes
|
|
10
|
+
----------
|
|
11
|
+
|
|
12
|
+
.. autosummary::
|
|
13
|
+
:toctree: ../stubs/
|
|
14
|
+
|
|
15
|
+
QbraidClient
|
|
16
|
+
QbraidSession
|
|
17
|
+
PostForcelistRetry
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
Functions
|
|
21
|
+
----------
|
|
22
|
+
|
|
23
|
+
.. autosummary::
|
|
24
|
+
:toctree: ../stubs/
|
|
25
|
+
|
|
26
|
+
client
|
|
27
|
+
register_client
|
|
28
|
+
discover_services
|
|
29
|
+
setup_default_session
|
|
30
|
+
|
|
31
|
+
Exceptions
|
|
32
|
+
------------
|
|
33
|
+
|
|
34
|
+
.. autosummary::
|
|
35
|
+
:toctree: ../stubs/
|
|
36
|
+
|
|
37
|
+
QbraidException
|
|
38
|
+
AuthError
|
|
39
|
+
ConfigError
|
|
40
|
+
RequestsApiError
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
from .client import QbraidClient
|
|
44
|
+
from .exceptions import (
|
|
45
|
+
AuthError,
|
|
46
|
+
ConfigError,
|
|
47
|
+
QbraidException,
|
|
48
|
+
RequestsApiError,
|
|
49
|
+
ResourceNotFoundError,
|
|
50
|
+
UserNotFoundError,
|
|
51
|
+
)
|
|
52
|
+
from .registry import discover_services, register_client
|
|
53
|
+
from .retry import PostForcelistRetry
|
|
54
|
+
from .session import QbraidSession
|
|
55
|
+
|
|
56
|
+
# Hold the default session in a global variable, but don't initialize it yet.
|
|
57
|
+
_DEFAULT_SESSION = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def setup_default_session(**kwargs):
|
|
61
|
+
"""
|
|
62
|
+
Set up a default session, passing through any parameters to the session
|
|
63
|
+
constructor. There is no need to call this unless you wish to pass custom
|
|
64
|
+
parameters, because a default session will be created for you.
|
|
65
|
+
"""
|
|
66
|
+
global _DEFAULT_SESSION # pylint: disable=global-statement
|
|
67
|
+
_DEFAULT_SESSION = QbraidSession(**kwargs)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _get_default_session():
|
|
71
|
+
"""
|
|
72
|
+
Get or create a default session. If the session does not exist, it is created
|
|
73
|
+
with the provided keyword arguments. If it already exists, the existing session
|
|
74
|
+
is returned, ignoring any provided arguments.
|
|
75
|
+
|
|
76
|
+
This function ensures that a session is created only once and reused, offering
|
|
77
|
+
a balance between laziness and reusability.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
**kwargs: Keyword arguments to pass to the Session constructor if creating
|
|
81
|
+
a new session.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
The default session instance.
|
|
85
|
+
"""
|
|
86
|
+
if _DEFAULT_SESSION is None:
|
|
87
|
+
setup_default_session()
|
|
88
|
+
|
|
89
|
+
return _DEFAULT_SESSION
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def client(*args, **kwargs):
|
|
93
|
+
"""
|
|
94
|
+
Create a client for a specified service using the default session. If specific
|
|
95
|
+
session parameters are needed, a new default session can be initialized before
|
|
96
|
+
calling this function.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
service_name (str): The name of the service for which to create the client.
|
|
100
|
+
**kwargs: Keyword arguments for session customization, used only if creating
|
|
101
|
+
a new default session.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
A service client instance.
|
|
105
|
+
"""
|
|
106
|
+
return _get_default_session().client(*args, **kwargs)
|
|
@@ -12,5 +12,5 @@ __version__: str
|
|
|
12
12
|
__version_tuple__: VERSION_TUPLE
|
|
13
13
|
version_tuple: VERSION_TUPLE
|
|
14
14
|
|
|
15
|
-
__version__ = version = '0.1.dev1+
|
|
16
|
-
__version_tuple__ = version_tuple = (0, 1, 'dev1', '
|
|
15
|
+
__version__ = version = '0.1.dev1+g2b36ca5'
|
|
16
|
+
__version_tuple__ = version_tuple = (0, 1, 'dev1', 'g2b36ca5')
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Copyright (c) 2024, qBraid Development Team
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Module defining abstract base clas for qBraid micro-service clients.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
import datetime
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from .exceptions import AuthError, RequestsApiError, ResourceNotFoundError, UserNotFoundError
|
|
14
|
+
from .session import QbraidSession
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class QbraidClient:
|
|
18
|
+
"""Base class for qBraid micro-service clients."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, session: Optional[QbraidSession] = None):
|
|
21
|
+
self.session = session or QbraidSession()
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def session(self) -> QbraidSession:
|
|
25
|
+
"""The QbraidSession used to make requests."""
|
|
26
|
+
return self._session
|
|
27
|
+
|
|
28
|
+
@session.setter
|
|
29
|
+
def session(self, value: Optional[QbraidSession]) -> None:
|
|
30
|
+
"""Set the QbraidSession.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
AuthError: If the provided session is not valid.
|
|
34
|
+
"""
|
|
35
|
+
qbraid_session = value or QbraidSession()
|
|
36
|
+
try:
|
|
37
|
+
_ = qbraid_session.get_user()
|
|
38
|
+
except UserNotFoundError as err:
|
|
39
|
+
raise AuthError("Access denied due to missing or invalid credentials") from err
|
|
40
|
+
self._session = qbraid_session
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _is_valid_object_id(candidate_id: str) -> bool:
|
|
44
|
+
"""
|
|
45
|
+
Check if the provided string is a valid MongoDB ObjectId format.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
candidate_id (str): The string to check.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
bool: True if the string is a valid ObjectId format, False otherwise.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
return bool(re.match(r"^[0-9a-fA-F]{24}$", candidate_id))
|
|
55
|
+
except (TypeError, SyntaxError):
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def _convert_email_symbols(email: str) -> Optional[str]:
|
|
60
|
+
"""Convert email to compatible string format"""
|
|
61
|
+
return (
|
|
62
|
+
email.replace("-", "-2d")
|
|
63
|
+
.replace(".", "-2e")
|
|
64
|
+
.replace("@", "-40")
|
|
65
|
+
.replace("_", "-5f")
|
|
66
|
+
.replace("+", "-2b")
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def _running_in_lab(self) -> bool:
|
|
70
|
+
"""Check if running in the qBraid Lab environment."""
|
|
71
|
+
# API interaction to confirm environment
|
|
72
|
+
try:
|
|
73
|
+
utc_datetime = datetime.datetime.now(datetime.UTC)
|
|
74
|
+
except AttributeError: # deprecated but use as fallback if datetime.UTC is not available
|
|
75
|
+
utc_datetime = datetime.datetime.utcnow()
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
formatted_time = utc_datetime.strftime("%Y%m%d%H%M%S")
|
|
79
|
+
directory = os.path.join(os.path.expanduser("~"), ".qbraid", "certs")
|
|
80
|
+
filepath = os.path.join(directory, formatted_time)
|
|
81
|
+
os.makedirs(directory, exist_ok=True)
|
|
82
|
+
|
|
83
|
+
# Create an empty file
|
|
84
|
+
with open(filepath, "w", encoding="utf-8"):
|
|
85
|
+
pass # The file is created and closed immediately
|
|
86
|
+
|
|
87
|
+
response = self.session.get(f"/lab/is-mounted/{formatted_time}")
|
|
88
|
+
is_mounted = bool(response.json().get("isMounted", False))
|
|
89
|
+
except (RequestsApiError, KeyError):
|
|
90
|
+
is_mounted = False
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
os.remove(filepath)
|
|
94
|
+
except (FileNotFoundError, IOError):
|
|
95
|
+
pass
|
|
96
|
+
return is_mounted
|
|
97
|
+
|
|
98
|
+
def user_credits_value(self) -> float:
|
|
99
|
+
"""
|
|
100
|
+
Get the current user's qBraid credits value.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
float: The current user's qBraid credits value.
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
res = self.session.get("/billing/credits/get-user-credits").json()
|
|
107
|
+
credits_value = res["qbraidCredits"]
|
|
108
|
+
return float(credits_value)
|
|
109
|
+
except (RequestsApiError, KeyError, ValueError) as err:
|
|
110
|
+
raise ResourceNotFoundError("Credits value not found") from err
|
|
@@ -34,3 +34,11 @@ class ConfigError(QbraidException):
|
|
|
34
34
|
|
|
35
35
|
class RequestsApiError(QbraidException):
|
|
36
36
|
"""Exception re-raising a RequestException."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ResourceNotFoundError(QbraidException):
|
|
40
|
+
"""Exception re-raising a RequestException."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class UserNotFoundError(ResourceNotFoundError):
|
|
44
|
+
"""Exception raised when a user is not found."""
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Copyright (c) 2024, qBraid Development Team
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Module for registering qBraid service clients.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
import importlib
|
|
9
|
+
import inspect
|
|
10
|
+
import os
|
|
11
|
+
import pkgutil
|
|
12
|
+
from typing import Callable, Dict, Iterator, Optional, Type
|
|
13
|
+
|
|
14
|
+
client_registry: Dict[str, Type] = {}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register_client(service_name: Optional[str] = None) -> Callable:
|
|
18
|
+
"""
|
|
19
|
+
Decorator to register a client class under a given service name.
|
|
20
|
+
|
|
21
|
+
If the service name is not explicitly provided, the decorator attempts to infer the service name
|
|
22
|
+
based on the directory name of the module where the decorated class is defined. This is useful
|
|
23
|
+
for automating the registration of service clients based on their directory structure.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
service_name (Optional[str]): The name of the service to register the client under. If None,
|
|
27
|
+
the name is inferred from the module's directory name.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Callable: A decorator that registers the decorated class in the global `client_registry`
|
|
31
|
+
under the determined service name.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
ValueError: If the service name could not be determined automatically.
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
@register_client()
|
|
38
|
+
class Service1Client:
|
|
39
|
+
# Implementation of the client
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def decorator(cls: Type) -> Type:
|
|
43
|
+
nonlocal service_name
|
|
44
|
+
if service_name is None:
|
|
45
|
+
# Inspect the stack to find the module of the caller
|
|
46
|
+
frame = inspect.stack()[1]
|
|
47
|
+
module = inspect.getmodule(frame[0])
|
|
48
|
+
if module is not None and hasattr(module, "__file__"):
|
|
49
|
+
# Extract the service name from the module's file path
|
|
50
|
+
service_dir = os.path.basename(os.path.dirname(module.__file__))
|
|
51
|
+
service_name = service_dir
|
|
52
|
+
else:
|
|
53
|
+
raise ValueError("Could not determine the service name automatically.")
|
|
54
|
+
|
|
55
|
+
# Register the class with the determined service name
|
|
56
|
+
client_registry[service_name] = cls
|
|
57
|
+
|
|
58
|
+
return cls
|
|
59
|
+
|
|
60
|
+
return decorator
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def discover_services(directory: str) -> Iterator[str]:
|
|
64
|
+
"""
|
|
65
|
+
Discover and import service client modules to trigger registration.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
directory (str): Directory path to search for service modules.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Iterator over the names of the discovered services.
|
|
72
|
+
"""
|
|
73
|
+
for _, name, ispkg in pkgutil.iter_modules([directory]):
|
|
74
|
+
if ispkg:
|
|
75
|
+
package = f"qbraid_core.services.{name}"
|
|
76
|
+
yield name
|
|
77
|
+
importlib.import_module(package)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Copyright (c) 2024, qBraid Development Team
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Module containing custom ``urllib3.Retry`` class
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from urllib3.util.retry import Retry
|
|
12
|
+
|
|
13
|
+
STATUS_FORCELIST = (
|
|
14
|
+
500, # General server error
|
|
15
|
+
502, # Bad Gateway
|
|
16
|
+
503, # Service Unavailable
|
|
17
|
+
504, # Gateway Timeout
|
|
18
|
+
520, # Cloudflare general error
|
|
19
|
+
522, # Cloudflare connection timeout
|
|
20
|
+
524, # Cloudflare Timeout
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PostForcelistRetry(Retry):
|
|
27
|
+
"""Custom :py:class:`urllib3.Retry` class that performs retry on ``POST`` errors in the
|
|
28
|
+
force list. Retrying of ``POST`` requests are allowed *only* when the status code returned
|
|
29
|
+
is on the :py:const:`~qbraid_core.retry.STATUS_FORCELIST`. While ``POST`` requests are
|
|
30
|
+
recommended not to be retried due to not being idempotent, retrying on specific 5xx errors
|
|
31
|
+
through the qBraid API is safe.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def increment( # type: ignore[no-untyped-def]
|
|
35
|
+
self,
|
|
36
|
+
method=None,
|
|
37
|
+
url=None,
|
|
38
|
+
response=None,
|
|
39
|
+
error=None,
|
|
40
|
+
_pool=None,
|
|
41
|
+
_stacktrace=None,
|
|
42
|
+
):
|
|
43
|
+
"""Overwrites parent class increment method for logging."""
|
|
44
|
+
if logger.getEffectiveLevel() is logging.DEBUG:
|
|
45
|
+
# coverage: ignore
|
|
46
|
+
status = data = headers = None
|
|
47
|
+
if response:
|
|
48
|
+
status = response.status
|
|
49
|
+
data = response.data
|
|
50
|
+
headers = response.headers
|
|
51
|
+
logger.debug(
|
|
52
|
+
"Retrying method=%s, url=%s, status=%s, error=%s, data=%s, headers=%s",
|
|
53
|
+
method,
|
|
54
|
+
url,
|
|
55
|
+
status,
|
|
56
|
+
error,
|
|
57
|
+
data,
|
|
58
|
+
headers,
|
|
59
|
+
)
|
|
60
|
+
return super().increment(
|
|
61
|
+
method=method,
|
|
62
|
+
url=url,
|
|
63
|
+
response=response,
|
|
64
|
+
error=error,
|
|
65
|
+
_pool=_pool,
|
|
66
|
+
_stacktrace=_stacktrace,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def is_retry(self, method: str, status_code: int, has_retry_after: bool = False) -> bool:
|
|
70
|
+
"""Indicate whether the request should be retried.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
method: Request method.
|
|
74
|
+
status_code: Status code.
|
|
75
|
+
has_retry_after: Whether retry has been done before.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
``True`` if the request should be retried, ``False`` otherwise.
|
|
79
|
+
"""
|
|
80
|
+
if method.upper() == "POST" and status_code in self.status_forcelist:
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
return super().is_retry(method, status_code, has_retry_after)
|
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
# All rights reserved.
|
|
3
3
|
|
|
4
4
|
"""
|
|
5
|
-
Module for interfacing with qBraid environments
|
|
5
|
+
Module providing utilities for interfacing with qBraid environments
|
|
6
6
|
|
|
7
|
-
.. currentmodule:: qbraid_core.environments
|
|
7
|
+
.. currentmodule:: qbraid_core.services.environments
|
|
8
|
+
|
|
9
|
+
Classes
|
|
10
|
+
----------
|
|
11
|
+
|
|
12
|
+
.. autosummary::
|
|
13
|
+
:toctree: ../stubs/
|
|
14
|
+
|
|
15
|
+
EnvironmentManagerClient
|
|
8
16
|
|
|
9
17
|
Functions
|
|
10
18
|
----------
|
|
@@ -12,10 +20,10 @@ Functions
|
|
|
12
20
|
.. autosummary::
|
|
13
21
|
:toctree: ../stubs/
|
|
14
22
|
|
|
15
|
-
|
|
23
|
+
create_local_venv
|
|
16
24
|
install_status_codes
|
|
17
25
|
update_install_status
|
|
18
|
-
|
|
26
|
+
get_default_envs_paths
|
|
19
27
|
get_env_path
|
|
20
28
|
get_tmp_dir_names
|
|
21
29
|
get_next_tmpn
|
|
@@ -29,15 +37,17 @@ Exceptions
|
|
|
29
37
|
.. autosummary::
|
|
30
38
|
:toctree: ../stubs/
|
|
31
39
|
|
|
32
|
-
|
|
40
|
+
EnvironmentServiceRequestError
|
|
41
|
+
EnvironmentServiceRuntimeError
|
|
33
42
|
|
|
34
43
|
"""
|
|
35
|
-
from .
|
|
36
|
-
from .
|
|
44
|
+
from .client import EnvironmentManagerClient
|
|
45
|
+
from .create import create_local_venv
|
|
46
|
+
from .exceptions import EnvironmentServiceRequestError, EnvironmentServiceRuntimeError
|
|
37
47
|
from .paths import (
|
|
48
|
+
get_default_envs_paths,
|
|
38
49
|
get_env_path,
|
|
39
50
|
get_next_tmpn,
|
|
40
|
-
get_qbraid_envs_paths,
|
|
41
51
|
get_tmp_dir_names,
|
|
42
52
|
which_python,
|
|
43
53
|
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Copyright (c) 2024, qBraid Development Team
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Module providing client for interacting with qBraid quantum services.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from qbraid_core.client import QbraidClient
|
|
12
|
+
from qbraid_core.exceptions import RequestsApiError
|
|
13
|
+
from qbraid_core.registry import register_client
|
|
14
|
+
|
|
15
|
+
from .exceptions import EnvironmentServiceRequestError
|
|
16
|
+
from .paths import get_default_envs_paths
|
|
17
|
+
from .validate import is_valid_env_name, is_valid_slug
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@register_client()
|
|
21
|
+
class EnvironmentManagerClient(QbraidClient):
|
|
22
|
+
"""Client for interacting with qBraid environment services."""
|
|
23
|
+
|
|
24
|
+
# envs_paths = Optional[Union[Path, str]] = None
|
|
25
|
+
def __init__(self, *args, **kwargs):
|
|
26
|
+
super().__init__(*args, **kwargs)
|
|
27
|
+
self.envs_paths = get_default_envs_paths()
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def envs_paths(self) -> List[Path]:
|
|
31
|
+
"""Returns a dictionary of environment paths.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
A dictionary containing the environment paths.
|
|
35
|
+
"""
|
|
36
|
+
return self._envs_paths
|
|
37
|
+
|
|
38
|
+
@envs_paths.setter
|
|
39
|
+
def envs_paths(self, value: List[Path]):
|
|
40
|
+
"""Sets the qBraid environments paths."""
|
|
41
|
+
self._envs_paths = value
|
|
42
|
+
|
|
43
|
+
def create_environment(self, name: str, description: Optional[str] = None) -> Dict[str, Any]:
|
|
44
|
+
"""Creates a new environment with the given name and description.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
name: The name of the environment to create.
|
|
48
|
+
description: Optional description of the environment.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
A dictionary containing the environment data.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
ValueError: If the environment name is invalid or the description is too long.
|
|
55
|
+
EnvironmentServiceRequestError: If the create environment request fails.
|
|
56
|
+
"""
|
|
57
|
+
if not is_valid_env_name(name):
|
|
58
|
+
raise ValueError(f"Invalid environment name: {name}")
|
|
59
|
+
|
|
60
|
+
if description and len(description) > 300:
|
|
61
|
+
raise ValueError("Description is too long. Maximum length is 300 characters.")
|
|
62
|
+
|
|
63
|
+
req_body = {
|
|
64
|
+
"name": name,
|
|
65
|
+
"description": description or "",
|
|
66
|
+
"tags": "", # comma separated list of tags
|
|
67
|
+
"code": "", # newline separated list of packages
|
|
68
|
+
"visibility": "private",
|
|
69
|
+
"kernelName": "",
|
|
70
|
+
"prompt": "",
|
|
71
|
+
"origin": "CLI", # TODO: change to "CORE"
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
env_data = self.session.post("/environments/create", json=req_body).json()
|
|
76
|
+
except RequestsApiError as err:
|
|
77
|
+
raise EnvironmentServiceRequestError("Create environment request failed") from err
|
|
78
|
+
|
|
79
|
+
if env_data is None or len(env_data) == 0 or env_data.get("slug") is None:
|
|
80
|
+
raise EnvironmentServiceRequestError(
|
|
81
|
+
"Create environment request responsed with invalid environment data"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return env_data
|
|
85
|
+
|
|
86
|
+
def delete_environment(self, slug: str) -> None:
|
|
87
|
+
"""Deletes the environment with the given slug.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
slug: The slug of the environment to delete.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
None
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
ValueError: If the environment slug is invalid.
|
|
97
|
+
EnvironmentServiceRequestError: If the delete environment request fails.
|
|
98
|
+
"""
|
|
99
|
+
if not is_valid_slug(slug):
|
|
100
|
+
raise ValueError(f"Invalid environment slug: {slug}")
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
self.session.delete(f"/environments/{slug}")
|
|
104
|
+
except RequestsApiError as err:
|
|
105
|
+
raise EnvironmentServiceRequestError("Delete environment request failed") from err
|
|
@@ -21,7 +21,7 @@ logging.basicConfig(
|
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def
|
|
24
|
+
def create_local_venv(slug_path: Union[str, Path], prompt: str) -> None:
|
|
25
25
|
"""Create virtual environment and swap PS1 display name."""
|
|
26
26
|
try:
|
|
27
27
|
# Ensure slug_path is a Path object
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Copyright (c) 2024, qBraid Development Team
|
|
2
|
+
# All rights reserved.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Module defining custom exceptions for the qBraid environments module.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from qbraid_core.exceptions import QbraidException
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EnvironmentServiceRequestError(QbraidException):
|
|
13
|
+
"""Base class for errors raised by API requests made through qBraid environment
|
|
14
|
+
service clients."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EnvironmentServiceRuntimeError(QbraidException):
|
|
18
|
+
"""Base class for runtime errors raised by qBraid quantum service clients."""
|