cisco-eox-query 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,55 @@
1
+ """Client for the Cisco End-of-Life (EOX) API.
2
+
3
+ Versioning
4
+ ----------
5
+ Library major versions track the EOX API version:
6
+
7
+ - ``cisco-eox-query`` 1.x implements the EOX API **v5**.
8
+ - ``cisco-eox-query`` 2.x will implement the EOX API **v6** (when released).
9
+
10
+ The versioned implementation lives in :mod:`cisco_eox_query.v5`; this module
11
+ re-exports it as the default. Pin an explicit version with::
12
+
13
+ from cisco_eox_query.v5 import EOXClient
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from importlib.metadata import PackageNotFoundError, version
19
+
20
+ from cisco_eox_query._base import PaginationError, RateLimitError, RetryError
21
+ from cisco_eox_query.v5 import (
22
+ EOX_ATTRIBS,
23
+ OS_TYPES,
24
+ EOXAPIError,
25
+ EOXClient,
26
+ EOXErrorInfo,
27
+ EOXRecord,
28
+ EOXResponse,
29
+ MigrationDetails,
30
+ PaginationResponseRecord,
31
+ )
32
+
33
+ try:
34
+ __version__ = version("cisco-eox-query")
35
+ except PackageNotFoundError:
36
+ __version__ = "1.0.0"
37
+
38
+ SUPPORTED_API_VERSION = 5
39
+
40
+ __all__ = [
41
+ "EOXClient",
42
+ "EOXRecord",
43
+ "EOXResponse",
44
+ "EOXErrorInfo",
45
+ "EOXAPIError",
46
+ "MigrationDetails",
47
+ "PaginationResponseRecord",
48
+ "RetryError",
49
+ "RateLimitError",
50
+ "PaginationError",
51
+ "EOX_ATTRIBS",
52
+ "OS_TYPES",
53
+ "SUPPORTED_API_VERSION",
54
+ "__version__",
55
+ ]
@@ -0,0 +1,279 @@
1
+ """Shared transport, retries, and OAuth2 client-credentials authentication.
2
+
3
+ All Cisco Support APIs authenticate with a bearer token obtained from the
4
+ Cisco API Console using the client-credentials grant, and are subject to rate
5
+ limiting. This base class encapsulates token acquisition/caching/refresh,
6
+ request throttling, and retry handling so each versioned API client only
7
+ concerns itself with endpoints and models.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import time
14
+ from datetime import datetime, timezone
15
+ from email.utils import parsedate_to_datetime
16
+ from http import HTTPStatus
17
+ from typing import Any, Self
18
+
19
+ import httpx
20
+
21
+ from cisco_eox_query.constants import (
22
+ BASE_URL,
23
+ DEFAULT_MAX_RETRIES,
24
+ DEFAULT_MIN_REQUEST_INTERVAL,
25
+ DEFAULT_RETRY_DELAY,
26
+ DEFAULT_TIMEOUT,
27
+ MAX_REQUESTS_PER_DAY,
28
+ MAX_REQUESTS_PER_SECOND,
29
+ TOKEN_URL,
30
+ )
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ _RETRYABLE_STATUS_CODES = frozenset(
35
+ {
36
+ HTTPStatus.REQUEST_TIMEOUT, # 408
37
+ HTTPStatus.TOO_EARLY, # 425
38
+ HTTPStatus.TOO_MANY_REQUESTS, # 429
39
+ HTTPStatus.INTERNAL_SERVER_ERROR, # 500
40
+ HTTPStatus.BAD_GATEWAY, # 502
41
+ HTTPStatus.SERVICE_UNAVAILABLE, # 503
42
+ HTTPStatus.GATEWAY_TIMEOUT, # 504
43
+ }
44
+ )
45
+
46
+
47
+ class RetryError(Exception):
48
+ """Raised when an API request fails after exhausting all retries."""
49
+
50
+
51
+ class RateLimitError(RetryError):
52
+ """Raised when a rate-limited request fails after exhausting all retries."""
53
+
54
+
55
+ class PaginationError(RetryError):
56
+ """Raised when pagination does not terminate within the page limit."""
57
+
58
+
59
+ def _retry_after_seconds(response: httpx.Response, default: float) -> float:
60
+ value = response.headers.get("Retry-After")
61
+ if not value:
62
+ return default
63
+ try:
64
+ return max(0.0, float(value))
65
+ except ValueError:
66
+ pass
67
+ try:
68
+ retry_at = parsedate_to_datetime(value)
69
+ if retry_at.tzinfo is None:
70
+ retry_at = retry_at.replace(tzinfo=timezone.utc)
71
+ return max(0.0, (retry_at - datetime.now(timezone.utc)).total_seconds())
72
+ except (TypeError, ValueError):
73
+ return default
74
+
75
+
76
+ class SupportClient:
77
+ """Base client providing authenticated, rate-limit-aware httpx transport.
78
+
79
+ Args:
80
+ client_id, client_secret: Cisco API Console credentials used to obtain
81
+ and refresh a bearer token via the client-credentials grant.
82
+ access_token: an already-obtained bearer token (skips token fetch).
83
+ base_url, token_url: service endpoints (defaults are current).
84
+ timeout: per-request httpx timeout.
85
+ max_retries: number of retries for transient failures (HTTP 429/5xx or
86
+ transport errors) before giving up.
87
+ retry_delay: base seconds between retries; the ``Retry-After`` response
88
+ header takes precedence when present.
89
+ min_request_interval: minimum seconds between requests to stay under
90
+ the API's requests-per-second limit (``0`` disables throttling).
91
+ transport: optional httpx transport (used by tests).
92
+ """
93
+
94
+ def __init__(
95
+ self,
96
+ client_id: str | None = None,
97
+ client_secret: str | None = None,
98
+ access_token: str | None = None,
99
+ *,
100
+ base_url: str = BASE_URL,
101
+ token_url: str = TOKEN_URL,
102
+ timeout: httpx.Timeout | float = DEFAULT_TIMEOUT,
103
+ max_retries: int = DEFAULT_MAX_RETRIES,
104
+ retry_delay: float = DEFAULT_RETRY_DELAY,
105
+ min_request_interval: float = DEFAULT_MIN_REQUEST_INTERVAL,
106
+ transport: httpx.BaseTransport | None = None,
107
+ ) -> None:
108
+ if access_token is None and (client_id is None or client_secret is None):
109
+ raise ValueError("provide access_token or client_id with client_secret")
110
+ if max_retries < 0:
111
+ raise ValueError("max_retries must be >= 0")
112
+ self.client_id = client_id
113
+ self.client_secret = client_secret
114
+ self.token_url = token_url
115
+ self.max_retries = max_retries
116
+ self.retry_delay = retry_delay
117
+ self.min_request_interval = min_request_interval
118
+ self._access_token = access_token
119
+ self._token_expiry: float | None = None
120
+ self._next_request_at = 0.0
121
+ self._client = httpx.Client(base_url=base_url, timeout=timeout, transport=transport)
122
+ if self._access_token is None:
123
+ self._fetch_token()
124
+
125
+ def _fetch_token(self) -> None:
126
+ logger.debug("requesting access token from %s", self.token_url)
127
+ response = self._request_with_retry(
128
+ "POST",
129
+ self.token_url,
130
+ data={
131
+ "grant_type": "client_credentials",
132
+ "client_id": self.client_id,
133
+ "client_secret": self.client_secret,
134
+ },
135
+ headers={
136
+ "Accept": "application/json",
137
+ "Content-Type": "application/x-www-form-urlencoded",
138
+ },
139
+ )
140
+ if response.status_code >= 400:
141
+ logger.error("token request failed with %s: %s", response.status_code, response.text[:300])
142
+ response.raise_for_status()
143
+ try:
144
+ payload = response.json()
145
+ except ValueError as exc:
146
+ raise ValueError(
147
+ f"invalid JSON from token endpoint {self.token_url}: {exc}"
148
+ ) from exc
149
+ if "access_token" not in payload:
150
+ raise ValueError(
151
+ f"token endpoint {self.token_url} returned no access_token "
152
+ f"(HTTP {response.status_code})"
153
+ )
154
+ self._access_token = payload["access_token"]
155
+ expires_in = float(payload.get("expires_in", 3600))
156
+ self._token_expiry = time.monotonic() + expires_in - 300
157
+ logger.info("obtained access token (expires_in=%ss)", expires_in)
158
+
159
+ def _ensure_token(self) -> None:
160
+ if self._access_token is None:
161
+ self._fetch_token()
162
+ elif (
163
+ self.client_id is not None
164
+ and self._token_expiry is not None
165
+ and time.monotonic() >= self._token_expiry
166
+ ):
167
+ logger.debug("access token expired, refreshing")
168
+ self._fetch_token()
169
+
170
+ def _throttle(self) -> None:
171
+ if self.min_request_interval <= 0:
172
+ return
173
+ now = time.monotonic()
174
+ wait = self._next_request_at - now
175
+ if wait > 0:
176
+ logger.debug("throttling request for %.2fs", wait)
177
+ time.sleep(wait)
178
+ self._next_request_at = time.monotonic() + self.min_request_interval
179
+
180
+ def _request_with_retry(
181
+ self,
182
+ method: str,
183
+ url: str,
184
+ *,
185
+ params: dict[str, Any] | None = None,
186
+ headers: dict[str, str] | None = None,
187
+ data: dict[str, str] | None = None,
188
+ ) -> httpx.Response:
189
+ request = self._client.build_request(method, url, params=params, headers=headers, data=data)
190
+ attempts = self.max_retries + 1
191
+ for attempt in range(attempts):
192
+ self._throttle()
193
+ try:
194
+ response = self._client.send(request)
195
+ except httpx.TransportError as exc:
196
+ if attempt >= self.max_retries:
197
+ raise RetryError(
198
+ f"request to {url} failed after {self.max_retries} retries: {exc}"
199
+ ) from exc
200
+ logger.warning(
201
+ "transport error (attempt %d/%d), retrying in %.1fs: %s",
202
+ attempt + 1,
203
+ attempts,
204
+ self.retry_delay,
205
+ exc,
206
+ )
207
+ time.sleep(self.retry_delay)
208
+ continue
209
+ if response.status_code not in _RETRYABLE_STATUS_CODES:
210
+ return response
211
+ if attempt >= self.max_retries:
212
+ if response.status_code == HTTPStatus.TOO_MANY_REQUESTS:
213
+ raise RateLimitError(
214
+ f"exceeded the Cisco API rate limit after {self.max_retries} retries; "
215
+ f"this is likely the daily limit of {MAX_REQUESTS_PER_DAY} requests/day "
216
+ f"(or the {MAX_REQUESTS_PER_SECOND} requests/second burst limit); "
217
+ "please wait and try again later"
218
+ )
219
+ raise RetryError(
220
+ f"request to {url} returned HTTP {response.status_code} "
221
+ f"after {self.max_retries} retries"
222
+ )
223
+ delay = _retry_after_seconds(response, self.retry_delay)
224
+ logger.warning(
225
+ "HTTP %s (attempt %d/%d), retrying in %.1fs",
226
+ response.status_code,
227
+ attempt + 1,
228
+ attempts,
229
+ delay,
230
+ )
231
+ time.sleep(delay)
232
+
233
+ def _get_json(self, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
234
+ self._ensure_token()
235
+ logger.debug("GET %s params=%s", path, params or {})
236
+ response = self._request_with_retry(
237
+ "GET",
238
+ path,
239
+ params=params or {},
240
+ headers={
241
+ "Authorization": f"Bearer {self._access_token}",
242
+ "Accept": "application/json",
243
+ },
244
+ )
245
+ if response.status_code == HTTPStatus.UNAUTHORIZED and self.client_id is not None:
246
+ logger.warning(
247
+ "GET %s returned %s, refreshing token and retrying once",
248
+ path,
249
+ response.status_code,
250
+ )
251
+ self._fetch_token()
252
+ response = self._request_with_retry(
253
+ "GET",
254
+ path,
255
+ params=params or {},
256
+ headers={
257
+ "Authorization": f"Bearer {self._access_token}",
258
+ "Accept": "application/json",
259
+ },
260
+ )
261
+ if response.status_code >= 400:
262
+ logger.error("GET %s failed with %s: %s", path, response.status_code, response.text[:300])
263
+ response.raise_for_status()
264
+ logger.debug("GET %s -> %s (%d bytes)", path, response.status_code, len(response.content))
265
+ try:
266
+ return response.json()
267
+ except ValueError as exc:
268
+ raise ValueError(
269
+ f"invalid JSON response from {path} (HTTP {response.status_code}): {exc}"
270
+ ) from exc
271
+
272
+ def close(self) -> None:
273
+ self._client.close()
274
+
275
+ def __enter__(self) -> Self:
276
+ return self
277
+
278
+ def __exit__(self, *exc_info: object) -> None:
279
+ self.close()
cisco_eox_query/cli.py ADDED
@@ -0,0 +1,272 @@
1
+ """Command-line interface for cisco-eox-query.
2
+
3
+ Installed as the ``eox-query`` console script.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import logging
10
+ import os
11
+ import sys
12
+ from typing import Sequence
13
+
14
+ import httpx
15
+
16
+ from cisco_eox_query import (
17
+ EOXAPIError,
18
+ EOXClient,
19
+ EOXRecord,
20
+ PaginationError,
21
+ RateLimitError,
22
+ RetryError,
23
+ __version__,
24
+ )
25
+ from cisco_eox_query.constants import DEFAULT_MAX_PAGES
26
+ from cisco_eox_query.examples import (
27
+ COMMAND_EXAMPLES,
28
+ ROOT_EXAMPLES,
29
+ format_epilog,
30
+ format_examples,
31
+ )
32
+
33
+ LOG_FORMAT = "%(asctime)s - %(levelname)s - %(name)s - %(message)s"
34
+ LOG_DATEFMT = "%Y-%m-%d %H:%M:%S"
35
+
36
+ EXIT_RATE_LIMIT = 2
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ class _ExamplesAction(argparse.Action):
42
+ """Print usage examples and exit (mirrors the ``--version`` action)."""
43
+
44
+ def __init__(
45
+ self,
46
+ option_strings,
47
+ dest,
48
+ nargs=0,
49
+ examples=None,
50
+ title=None,
51
+ **kwargs,
52
+ ):
53
+ super().__init__(option_strings, dest, nargs=nargs, **kwargs)
54
+ self.examples = examples or []
55
+ self.title = title
56
+
57
+ def __call__(self, parser, namespace, values, option_string=None):
58
+ print(format_examples(self.examples, title=self.title))
59
+ parser.exit()
60
+
61
+
62
+ def build_parser() -> argparse.ArgumentParser:
63
+ parser = argparse.ArgumentParser(
64
+ prog="eox-query",
65
+ description="Query the Cisco End-of-Life (EOX) API.",
66
+ epilog=format_epilog(ROOT_EXAMPLES)
67
+ + "\n\nRun 'eox-query --examples' for more examples.",
68
+ formatter_class=argparse.RawDescriptionHelpFormatter,
69
+ )
70
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
71
+ parser.add_argument(
72
+ "--examples",
73
+ action=_ExamplesAction,
74
+ examples=ROOT_EXAMPLES,
75
+ title="Examples",
76
+ help="show usage examples and exit",
77
+ )
78
+ parser.add_argument(
79
+ "--client-id",
80
+ default=os.environ.get("EOX_CLIENT_ID"),
81
+ help="Cisco API console client ID (or EOX_CLIENT_ID)",
82
+ )
83
+ parser.add_argument(
84
+ "--client-secret",
85
+ default=os.environ.get("EOX_CLIENT_SECRET"),
86
+ help="Cisco API console client secret (or EOX_CLIENT_SECRET)",
87
+ )
88
+ parser.add_argument(
89
+ "--access-token",
90
+ default=os.environ.get("EOX_ACCESS_TOKEN"),
91
+ help="pre-obtained bearer token (or EOX_ACCESS_TOKEN)",
92
+ )
93
+ parser.add_argument("--timeout", type=float, default=30.0, help="request timeout in seconds")
94
+ parser.add_argument(
95
+ "-v",
96
+ "--verbose",
97
+ action="count",
98
+ default=0,
99
+ help="increase log verbosity (-v info, -vv debug)",
100
+ )
101
+
102
+ subparsers = parser.add_subparsers(dest="command", required=True)
103
+
104
+ pid = subparsers.add_parser(
105
+ "pid",
106
+ help="search by product ID(s)",
107
+ description="Search the Cisco EOX API by product ID(s).",
108
+ epilog=format_epilog(COMMAND_EXAMPLES["pid"])
109
+ + "\n\nRun 'eox-query pid --examples' for more examples.",
110
+ formatter_class=argparse.RawDescriptionHelpFormatter,
111
+ )
112
+ pid.add_argument(
113
+ "--examples",
114
+ action=_ExamplesAction,
115
+ examples=COMMAND_EXAMPLES["pid"],
116
+ title="Examples for 'pid'",
117
+ help="show usage examples and exit",
118
+ )
119
+ pid.add_argument("product_ids", nargs="+", help="one or more PIDs (wildcards allowed)")
120
+
121
+ serial = subparsers.add_parser(
122
+ "serial",
123
+ help="search by serial number(s)",
124
+ description="Search the Cisco EOX API by serial number(s).",
125
+ epilog=format_epilog(COMMAND_EXAMPLES["serial"])
126
+ + "\n\nRun 'eox-query serial --examples' for more examples.",
127
+ formatter_class=argparse.RawDescriptionHelpFormatter,
128
+ )
129
+ serial.add_argument(
130
+ "--examples",
131
+ action=_ExamplesAction,
132
+ examples=COMMAND_EXAMPLES["serial"],
133
+ title="Examples for 'serial'",
134
+ help="show usage examples and exit",
135
+ )
136
+ serial.add_argument("serial_numbers", nargs="+", help="one or more serial numbers")
137
+
138
+ software = subparsers.add_parser(
139
+ "software",
140
+ help="search by software release string(s)",
141
+ description="Search the Cisco EOX API by software release string(s).",
142
+ epilog=format_epilog(COMMAND_EXAMPLES["software"])
143
+ + "\n\nRun 'eox-query software --examples' for more examples.",
144
+ formatter_class=argparse.RawDescriptionHelpFormatter,
145
+ )
146
+ software.add_argument(
147
+ "--examples",
148
+ action=_ExamplesAction,
149
+ examples=COMMAND_EXAMPLES["software"],
150
+ title="Examples for 'software'",
151
+ help="show usage examples and exit",
152
+ )
153
+ software.add_argument("releases", nargs="+", help="SWversion[,OSType] tuples, e.g. 12.4(15)T,IOS")
154
+
155
+ dates = subparsers.add_parser(
156
+ "dates",
157
+ help="search by date range",
158
+ description="Search the Cisco EOX API by date range.",
159
+ epilog=format_epilog(COMMAND_EXAMPLES["dates"])
160
+ + "\n\nRun 'eox-query dates --examples' for more examples.",
161
+ formatter_class=argparse.RawDescriptionHelpFormatter,
162
+ )
163
+ dates.add_argument(
164
+ "--examples",
165
+ action=_ExamplesAction,
166
+ examples=COMMAND_EXAMPLES["dates"],
167
+ title="Examples for 'dates'",
168
+ help="show usage examples and exit",
169
+ )
170
+ dates.add_argument("start", help="start date YYYY-MM-DD")
171
+ dates.add_argument("end", help="end date YYYY-MM-DD")
172
+ dates.add_argument("--attribs", help="comma-separated eoxAttrib values")
173
+
174
+ return parser
175
+
176
+
177
+ def setup_logging(level: int = logging.WARNING) -> None:
178
+ logging.basicConfig(level=level, format=LOG_FORMAT, datefmt=LOG_DATEFMT)
179
+
180
+
181
+ def _log_level(verbose: int) -> int:
182
+ if verbose >= 2:
183
+ return logging.DEBUG
184
+ if verbose == 1:
185
+ return logging.INFO
186
+ return logging.WARNING
187
+
188
+
189
+ def main(argv: Sequence[str] | None = None) -> int:
190
+ args = build_parser().parse_args(argv)
191
+ setup_logging(_log_level(args.verbose))
192
+ try:
193
+ with _build_client(args) as client:
194
+ records = _dispatch(client, args)
195
+ for record in records:
196
+ _print_record(record)
197
+ if records:
198
+ logger.info("found %d EOX record(s)", len(records))
199
+ else:
200
+ logger.warning("No EOX records found.")
201
+ except RateLimitError as exc:
202
+ logger.error("%s", exc)
203
+ return EXIT_RATE_LIMIT
204
+ except (ValueError, httpx.HTTPError, EOXAPIError, RetryError) as exc:
205
+ logger.error("%s", exc)
206
+ return 1
207
+ return 0
208
+
209
+
210
+ def _build_client(args: argparse.Namespace) -> EOXClient:
211
+ if args.access_token:
212
+ return EOXClient(access_token=args.access_token, timeout=args.timeout)
213
+ if args.client_id and args.client_secret:
214
+ return EOXClient(client_id=args.client_id, client_secret=args.client_secret, timeout=args.timeout)
215
+ raise ValueError(
216
+ "credentials required: --client-id and --client-secret "
217
+ "(or EOX_CLIENT_ID/EOX_CLIENT_SECRET), or --access-token"
218
+ )
219
+
220
+
221
+ def _dispatch(client: EOXClient, args: argparse.Namespace) -> list[EOXRecord]:
222
+ if args.command == "pid":
223
+ return _collect(client.search_by_product_ids, ",".join(args.product_ids))
224
+ if args.command == "serial":
225
+ return _collect(client.search_by_serial_numbers, ",".join(args.serial_numbers))
226
+ if args.command == "software":
227
+ return _collect(client.search_by_software_releases, *args.releases)
228
+ if args.command == "dates":
229
+ attribs = args.attribs.split(",") if args.attribs else None
230
+ return _collect(client.search_by_dates, args.start, args.end, attribs=attribs)
231
+ raise AssertionError(f"unhandled command: {args.command}")
232
+
233
+
234
+ def _collect(request_fn, *args, **kwargs) -> list[EOXRecord]:
235
+ records = []
236
+ page = 1
237
+ while True:
238
+ if page > DEFAULT_MAX_PAGES:
239
+ raise PaginationError(
240
+ f"pagination did not terminate after {DEFAULT_MAX_PAGES} pages"
241
+ )
242
+ response = request_fn(*args, page=page, **kwargs)
243
+ response.raise_for_error()
244
+ records.extend(response.records)
245
+ last = response.pagination.last_index if response.pagination and response.pagination.last_index else 1
246
+ if page >= last:
247
+ return records
248
+ page += 1
249
+
250
+
251
+ def _print_record(record: EOXRecord) -> None:
252
+ migration = record.migration_details
253
+ print(record.eol_product_id or "<no product id>")
254
+ for label, value in [
255
+ ("Product Description", record.product_id_description),
256
+ ("Bulletin", record.product_bulletin_number),
257
+ ("Bulletin Link", record.link_to_product_bulletin_url),
258
+ ("Announcement", record.eox_external_announcement_date),
259
+ ("End Of Sale", record.end_of_sale_date),
260
+ ("SW Maintenance Ends", record.end_of_sw_maintenance_releases),
261
+ ("Security Vulnerability Support Ends", record.end_of_security_vul_support_date),
262
+ ("Routine Failure Analysis Ends", record.end_of_routine_failure_analysis_date),
263
+ ("Service Contract Renewal", record.end_of_service_contract_renewal),
264
+ ("Last Date of Support", record.last_date_of_support),
265
+ ("Service Attach Ends", record.end_of_svc_attach_date),
266
+ ("Migration Product", migration.migration_information if migration else None),
267
+ ("Migration Product Info URL", migration.migration_product_info_url if migration else None),
268
+ ("Migration Product ID", migration.migration_product_id if migration else None),
269
+ ("Migration Strategy", migration.migration_strategy if migration else None),
270
+ ]:
271
+ if value is not None:
272
+ print(f" {label:<30} {value}")
@@ -0,0 +1,29 @@
1
+ """Constants shared across Cisco EOX API client versions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import httpx
6
+
7
+ BASE_URL = "https://apix.cisco.com"
8
+ TOKEN_URL = "https://id.cisco.com/oauth2/default/v1/token"
9
+ DEFAULT_TIMEOUT = httpx.Timeout(30.0)
10
+
11
+ MAX_REQUESTS_PER_SECOND = 5
12
+ MAX_REQUESTS_PER_DAY = 5000
13
+
14
+ DEFAULT_MAX_RETRIES = 3
15
+ DEFAULT_RETRY_DELAY = 5.0
16
+ DEFAULT_MIN_REQUEST_INTERVAL = 0.25
17
+ DEFAULT_MAX_PAGES = 1000
18
+
19
+ __all__ = [
20
+ "BASE_URL",
21
+ "TOKEN_URL",
22
+ "DEFAULT_TIMEOUT",
23
+ "MAX_REQUESTS_PER_SECOND",
24
+ "MAX_REQUESTS_PER_DAY",
25
+ "DEFAULT_MAX_RETRIES",
26
+ "DEFAULT_RETRY_DELAY",
27
+ "DEFAULT_MIN_REQUEST_INTERVAL",
28
+ "DEFAULT_MAX_PAGES",
29
+ ]