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.
Files changed (81) hide show
  1. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/.gitignore +1 -0
  2. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/PKG-INFO +5 -4
  3. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/README.md +4 -3
  4. qbraid-core-0.1.0.dev1/docs/api/qbraid_core.services.rst +16 -0
  5. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/index.rst +1 -3
  6. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/pyproject.toml +1 -1
  7. qbraid-core-0.1.0.dev1/qbraid_core/__init__.py +106 -0
  8. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core/_version.py +2 -2
  9. qbraid-core-0.1.0.dev1/qbraid_core/client.py +110 -0
  10. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core/exceptions.py +8 -0
  11. qbraid-core-0.1.0.dev1/qbraid_core/registry.py +77 -0
  12. qbraid-core-0.1.0.dev1/qbraid_core/retry.py +83 -0
  13. {qbraid-core-0.1.0.dev0/qbraid_core → qbraid-core-0.1.0.dev1/qbraid_core/services}/environments/__init__.py +18 -8
  14. qbraid-core-0.1.0.dev1/qbraid_core/services/environments/client.py +105 -0
  15. {qbraid-core-0.1.0.dev0/qbraid_core → qbraid-core-0.1.0.dev1/qbraid_core/services}/environments/create.py +1 -1
  16. qbraid-core-0.1.0.dev1/qbraid_core/services/environments/exceptions.py +18 -0
  17. {qbraid-core-0.1.0.dev0/qbraid_core → qbraid-core-0.1.0.dev1/qbraid_core/services}/environments/paths.py +38 -7
  18. qbraid-core-0.1.0.dev1/qbraid_core/services/quantum/__init__.py +41 -0
  19. qbraid-core-0.1.0.dev1/qbraid_core/services/quantum/adapter.py +120 -0
  20. qbraid-core-0.1.0.dev1/qbraid_core/services/quantum/client.py +146 -0
  21. qbraid-core-0.1.0.dev1/qbraid_core/services/quantum/exceptions.py +17 -0
  22. 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
  23. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core/session.py +63 -133
  24. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core.egg-info/PKG-INFO +5 -4
  25. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core.egg-info/SOURCES.txt +18 -16
  26. qbraid-core-0.1.0.dev1/tests/fixtures/__init__.py +0 -0
  27. qbraid-core-0.1.0.dev1/tests/test_client.py +48 -0
  28. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/tests/test_environments.py +8 -4
  29. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/tests/test_session.py +2 -16
  30. qbraid-core-0.1.0.dev0/docs/api/qbraid_core.devices.rst +0 -7
  31. qbraid-core-0.1.0.dev0/docs/api/qbraid_core.environments.rst +0 -7
  32. qbraid-core-0.1.0.dev0/docs/api/qbraid_core.jobs.rst +0 -7
  33. qbraid-core-0.1.0.dev0/qbraid_core/__init__.py +0 -30
  34. qbraid-core-0.1.0.dev0/qbraid_core/devices/__init__.py +0 -27
  35. qbraid-core-0.1.0.dev0/qbraid_core/devices/data.py +0 -76
  36. qbraid-core-0.1.0.dev0/qbraid_core/devices/exceptions.py +0 -13
  37. qbraid-core-0.1.0.dev0/qbraid_core/environments/exceptions.py +0 -13
  38. qbraid-core-0.1.0.dev0/qbraid_core/jobs/__init__.py +0 -29
  39. qbraid-core-0.1.0.dev0/qbraid_core/jobs/data.py +0 -48
  40. qbraid-core-0.1.0.dev0/qbraid_core/jobs/exceptions.py +0 -13
  41. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  42. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  43. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  44. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/.github/workflows/docs.yml +0 -0
  45. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/.github/workflows/format.yml +0 -0
  46. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/.github/workflows/main.yml +0 -0
  47. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/.github/workflows/publish.yml +0 -0
  48. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/.github/workflows/test-publish.yml +0 -0
  49. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/.readthedocs.yaml +0 -0
  50. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/LICENSE +0 -0
  51. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/MANIFEST.in +0 -0
  52. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/Makefile +0 -0
  53. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/_static/cards/jupyter.png +0 -0
  54. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/_static/cards/python.png +0 -0
  55. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/_static/cards/terminal.png +0 -0
  56. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/_static/css/custom.css +0 -0
  57. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/_static/css/s4defs-roles.css +0 -0
  58. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/_static/favicon.ico +0 -0
  59. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/_static/logo.png +0 -0
  60. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/api/qbraid_core.rst +0 -0
  61. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/api/qbraid_core.system.rst +0 -0
  62. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/conf.py +0 -0
  63. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/docs/make.bat +0 -0
  64. {qbraid-core-0.1.0.dev0/tests → qbraid-core-0.1.0.dev1/qbraid_core/services}/__init__.py +0 -0
  65. {qbraid-core-0.1.0.dev0/qbraid_core → qbraid-core-0.1.0.dev1/qbraid_core/services}/environments/state.py +0 -0
  66. {qbraid-core-0.1.0.dev0/qbraid_core → qbraid-core-0.1.0.dev1/qbraid_core/services}/environments/validate.py +0 -0
  67. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core/system/__init__.py +0 -0
  68. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core/system/exceptions.py +0 -0
  69. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core/system/executables.py +0 -0
  70. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core/system/generic.py +0 -0
  71. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core/system/packages.py +0 -0
  72. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core/system/threader.py +0 -0
  73. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core.egg-info/dependency_links.txt +0 -0
  74. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core.egg-info/requires.txt +0 -0
  75. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/qbraid_core.egg-info/top_level.txt +0 -0
  76. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/setup.cfg +0 -0
  77. {qbraid-core-0.1.0.dev0/tests/fixtures → qbraid-core-0.1.0.dev1/tests}/__init__.py +0 -0
  78. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/tests/conftest.py +0 -0
  79. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/tests/fixtures/environments.py +0 -0
  80. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/tests/test_system.py +0 -0
  81. {qbraid-core-0.1.0.dev0 → qbraid-core-0.1.0.dev1}/tools/verify_headers.py +0 -0
@@ -1,5 +1,6 @@
1
1
  # project files
2
2
  qbraid_core/_version.py
3
+ docs/stubs/
3
4
 
4
5
  # Byte-compiled / optimized / DLL files
5
6
  __pycache__/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qbraid-core
3
- Version: 0.1.0.dev0
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
- >>> from qbraid_core.devices import get_devices
92
- >>> device_data, _ = get_devices()
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
- >>> from qbraid_core.devices import get_devices
54
- >>> device_data, _ = get_devices()
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,16 @@
1
+ qbraid_core.services
2
+ ====================
3
+
4
+ .. automodule:: qbraid_core.services
5
+ :members:
6
+ :undoc-members:
7
+ :show-inheritance:
8
+
9
+ Submodules
10
+ -----------
11
+
12
+ .. autosummary::
13
+ :toctree: ../stubs/
14
+
15
+ environments
16
+ quantum
@@ -96,9 +96,7 @@ Documentation
96
96
  :hidden:
97
97
 
98
98
  api/qbraid_core
99
- api/qbraid_core.devices
100
- api/qbraid_core.environments
101
- api/qbraid_core.jobs
99
+ api/qbraid_core.services
102
100
  api/qbraid_core.system
103
101
 
104
102
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qbraid-core"
7
- version = "0.1.0.dev0"
7
+ version = "0.1.0.dev1"
8
8
  authors = [
9
9
  {name = "qBraid Development Team", email = "contact@qbraid.com"},
10
10
  ]
@@ -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+gd5f4c29'
16
- __version_tuple__ = version_tuple = (0, 1, 'dev1', 'gd5f4c29')
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
- create_venv
23
+ create_local_venv
16
24
  install_status_codes
17
25
  update_install_status
18
- get_qbraid_envs_paths
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
- QbraidEnvironmentError
40
+ EnvironmentServiceRequestError
41
+ EnvironmentServiceRuntimeError
33
42
 
34
43
  """
35
- from .create import create_venv
36
- from .exceptions import QbraidEnvironmentError
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 create_venv(slug_path: Union[str, Path], prompt: str) -> None:
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."""