caido-sdk-client 0.1.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.
Files changed (94) hide show
  1. caido_sdk_client-0.1.0/PKG-INFO +27 -0
  2. caido_sdk_client-0.1.0/README.md +15 -0
  3. caido_sdk_client-0.1.0/pyproject.toml +26 -0
  4. caido_sdk_client-0.1.0/src/caido_sdk_client/__init__.py +191 -0
  5. caido_sdk_client-0.1.0/src/caido_sdk_client/auth/__init__.py +27 -0
  6. caido_sdk_client-0.1.0/src/caido_sdk_client/auth/cache/__init__.py +6 -0
  7. caido_sdk_client-0.1.0/src/caido_sdk_client/auth/cache/file.py +71 -0
  8. caido_sdk_client-0.1.0/src/caido_sdk_client/auth/cache/types.py +19 -0
  9. caido_sdk_client-0.1.0/src/caido_sdk_client/auth/manager.py +196 -0
  10. caido_sdk_client-0.1.0/src/caido_sdk_client/auth/types.py +61 -0
  11. caido_sdk_client-0.1.0/src/caido_sdk_client/auth/utils.py +31 -0
  12. caido_sdk_client-0.1.0/src/caido_sdk_client/client.py +198 -0
  13. caido_sdk_client-0.1.0/src/caido_sdk_client/convert/__init__.py +25 -0
  14. caido_sdk_client-0.1.0/src/caido_sdk_client/convert/blob.py +19 -0
  15. caido_sdk_client-0.1.0/src/caido_sdk_client/convert/connection.py +27 -0
  16. caido_sdk_client-0.1.0/src/caido_sdk_client/convert/environment.py +27 -0
  17. caido_sdk_client-0.1.0/src/caido_sdk_client/convert/filter.py +17 -0
  18. caido_sdk_client-0.1.0/src/caido_sdk_client/convert/finding.py +25 -0
  19. caido_sdk_client-0.1.0/src/caido_sdk_client/convert/hosted_file.py +22 -0
  20. caido_sdk_client-0.1.0/src/caido_sdk_client/convert/instance_settings.py +58 -0
  21. caido_sdk_client-0.1.0/src/caido_sdk_client/convert/project.py +25 -0
  22. caido_sdk_client-0.1.0/src/caido_sdk_client/convert/request.py +56 -0
  23. caido_sdk_client-0.1.0/src/caido_sdk_client/convert/scope.py +18 -0
  24. caido_sdk_client-0.1.0/src/caido_sdk_client/errors/__init__.py +71 -0
  25. caido_sdk_client-0.1.0/src/caido_sdk_client/errors/all_errors.py +39 -0
  26. caido_sdk_client-0.1.0/src/caido_sdk_client/errors/auth.py +15 -0
  27. caido_sdk_client-0.1.0/src/caido_sdk_client/errors/authorization.py +40 -0
  28. caido_sdk_client-0.1.0/src/caido_sdk_client/errors/base.py +25 -0
  29. caido_sdk_client-0.1.0/src/caido_sdk_client/errors/cloud.py +24 -0
  30. caido_sdk_client-0.1.0/src/caido_sdk_client/errors/form.py +23 -0
  31. caido_sdk_client-0.1.0/src/caido_sdk_client/errors/from_error.py +82 -0
  32. caido_sdk_client-0.1.0/src/caido_sdk_client/errors/graphql.py +20 -0
  33. caido_sdk_client-0.1.0/src/caido_sdk_client/errors/health.py +15 -0
  34. caido_sdk_client-0.1.0/src/caido_sdk_client/errors/misc.py +23 -0
  35. caido_sdk_client-0.1.0/src/caido_sdk_client/errors/plugin.py +112 -0
  36. caido_sdk_client-0.1.0/src/caido_sdk_client/errors/project.py +30 -0
  37. caido_sdk_client-0.1.0/src/caido_sdk_client/errors/rest.py +23 -0
  38. caido_sdk_client-0.1.0/src/caido_sdk_client/errors/sdk.py +26 -0
  39. caido_sdk_client-0.1.0/src/caido_sdk_client/errors/tasks.py +15 -0
  40. caido_sdk_client-0.1.0/src/caido_sdk_client/errors/version.py +14 -0
  41. caido_sdk_client-0.1.0/src/caido_sdk_client/errors/workflow.py +24 -0
  42. caido_sdk_client-0.1.0/src/caido_sdk_client/graphql/__generated__/schema.py +2688 -0
  43. caido_sdk_client-0.1.0/src/caido_sdk_client/graphql/__init__.py +18 -0
  44. caido_sdk_client-0.1.0/src/caido_sdk_client/graphql/client.py +199 -0
  45. caido_sdk_client-0.1.0/src/caido_sdk_client/graphql/documents/connectionInfo.graphql +7 -0
  46. caido_sdk_client-0.1.0/src/caido_sdk_client/graphql/documents/environment.graphql +110 -0
  47. caido_sdk_client-0.1.0/src/caido_sdk_client/graphql/documents/errors.graphql +79 -0
  48. caido_sdk_client-0.1.0/src/caido_sdk_client/graphql/documents/filter.graphql +70 -0
  49. caido_sdk_client-0.1.0/src/caido_sdk_client/graphql/documents/finding.graphql +91 -0
  50. caido_sdk_client-0.1.0/src/caido_sdk_client/graphql/documents/hostedFile.graphql +37 -0
  51. caido_sdk_client-0.1.0/src/caido_sdk_client/graphql/documents/instanceSettings.graphql +55 -0
  52. caido_sdk_client-0.1.0/src/caido_sdk_client/graphql/documents/plugin.graphql +39 -0
  53. caido_sdk_client-0.1.0/src/caido_sdk_client/graphql/documents/project.graphql +103 -0
  54. caido_sdk_client-0.1.0/src/caido_sdk_client/graphql/documents/range.graphql +4 -0
  55. caido_sdk_client-0.1.0/src/caido_sdk_client/graphql/documents/replay.graphql +276 -0
  56. caido_sdk_client-0.1.0/src/caido_sdk_client/graphql/documents/request.graphql +81 -0
  57. caido_sdk_client-0.1.0/src/caido_sdk_client/graphql/documents/scope.graphql +59 -0
  58. caido_sdk_client-0.1.0/src/caido_sdk_client/graphql/documents/task.graphql +50 -0
  59. caido_sdk_client-0.1.0/src/caido_sdk_client/graphql/documents/viewer.graphql +30 -0
  60. caido_sdk_client-0.1.0/src/caido_sdk_client/graphql/documents/workflow.graphql +84 -0
  61. caido_sdk_client-0.1.0/src/caido_sdk_client/graphql/utils.py +72 -0
  62. caido_sdk_client-0.1.0/src/caido_sdk_client/logger.py +31 -0
  63. caido_sdk_client-0.1.0/src/caido_sdk_client/rest/__init__.py +7 -0
  64. caido_sdk_client-0.1.0/src/caido_sdk_client/rest/client.py +149 -0
  65. caido_sdk_client-0.1.0/src/caido_sdk_client/sdks/__init__.py +33 -0
  66. caido_sdk_client-0.1.0/src/caido_sdk_client/sdks/environment.py +267 -0
  67. caido_sdk_client-0.1.0/src/caido_sdk_client/sdks/filter.py +109 -0
  68. caido_sdk_client-0.1.0/src/caido_sdk_client/sdks/finding.py +139 -0
  69. caido_sdk_client-0.1.0/src/caido_sdk_client/sdks/hosted_file.py +79 -0
  70. caido_sdk_client-0.1.0/src/caido_sdk_client/sdks/instance.py +13 -0
  71. caido_sdk_client-0.1.0/src/caido_sdk_client/sdks/instance_settings.py +112 -0
  72. caido_sdk_client-0.1.0/src/caido_sdk_client/sdks/plugin.py +208 -0
  73. caido_sdk_client-0.1.0/src/caido_sdk_client/sdks/project.py +108 -0
  74. caido_sdk_client-0.1.0/src/caido_sdk_client/sdks/request.py +194 -0
  75. caido_sdk_client-0.1.0/src/caido_sdk_client/sdks/scope.py +101 -0
  76. caido_sdk_client-0.1.0/src/caido_sdk_client/sdks/user.py +85 -0
  77. caido_sdk_client-0.1.0/src/caido_sdk_client/types/__init__.py +127 -0
  78. caido_sdk_client-0.1.0/src/caido_sdk_client/types/connection.py +75 -0
  79. caido_sdk_client-0.1.0/src/caido_sdk_client/types/environment.py +55 -0
  80. caido_sdk_client-0.1.0/src/caido_sdk_client/types/filter.py +45 -0
  81. caido_sdk_client-0.1.0/src/caido_sdk_client/types/finding.py +43 -0
  82. caido_sdk_client-0.1.0/src/caido_sdk_client/types/hosted_file.py +45 -0
  83. caido_sdk_client-0.1.0/src/caido_sdk_client/types/instance_settings.py +113 -0
  84. caido_sdk_client-0.1.0/src/caido_sdk_client/types/plugin.py +70 -0
  85. caido_sdk_client-0.1.0/src/caido_sdk_client/types/project.py +33 -0
  86. caido_sdk_client-0.1.0/src/caido_sdk_client/types/request.py +69 -0
  87. caido_sdk_client-0.1.0/src/caido_sdk_client/types/scope.py +46 -0
  88. caido_sdk_client-0.1.0/src/caido_sdk_client/types/strings.py +57 -0
  89. caido_sdk_client-0.1.0/src/caido_sdk_client/types/user.py +58 -0
  90. caido_sdk_client-0.1.0/src/caido_sdk_client/utils/__init__.py +7 -0
  91. caido_sdk_client-0.1.0/src/caido_sdk_client/utils/errors.py +14 -0
  92. caido_sdk_client-0.1.0/src/caido_sdk_client/utils/list.py +101 -0
  93. caido_sdk_client-0.1.0/src/caido_sdk_client/utils/misc.py +10 -0
  94. caido_sdk_client-0.1.0/src/caido_sdk_client/utils/pydantic.py +6 -0
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.3
2
+ Name: caido-sdk-client
3
+ Version: 0.1.0
4
+ Summary: Client SDK for interacting with a Caido instance
5
+ Author: Caido Labs Inc.
6
+ Author-email: Caido Labs Inc. <dev@caido.io>
7
+ Requires-Dist: caido-server-auth>=0.1.2
8
+ Requires-Dist: gql[aiohttp,websockets]>=3.5.0
9
+ Requires-Dist: pydantic>=2.11.0
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+
13
+ <div align="center">
14
+ <img width="1000" alt="image" src="https://github.com/caido-community/.github/blob/main/content/banner.png?raw=true">
15
+
16
+ <br />
17
+ <br />
18
+ <a href="https://github.com/caido-community" target="_blank">Github</a>
19
+ <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
20
+ <a href="https://developer.caido.io/" target="_blank">Documentation</a>
21
+ <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
22
+ <a href="https://links.caido.io/www-discord" target="_blank">Discord</a>
23
+ <br />
24
+ <hr />
25
+ </div>
26
+
27
+ ## 👋 Client SDK
@@ -0,0 +1,15 @@
1
+ <div align="center">
2
+ <img width="1000" alt="image" src="https://github.com/caido-community/.github/blob/main/content/banner.png?raw=true">
3
+
4
+ <br />
5
+ <br />
6
+ <a href="https://github.com/caido-community" target="_blank">Github</a>
7
+ <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
8
+ <a href="https://developer.caido.io/" target="_blank">Documentation</a>
9
+ <span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
10
+ <a href="https://links.caido.io/www-discord" target="_blank">Discord</a>
11
+ <br />
12
+ <hr />
13
+ </div>
14
+
15
+ ## 👋 Client SDK
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "caido-sdk-client"
3
+ version = "0.1.0"
4
+ description = "Client SDK for interacting with a Caido instance"
5
+ readme = "README.md"
6
+ authors = [{ name = "Caido Labs Inc.", email = "dev@caido.io" }]
7
+ requires-python = ">=3.12"
8
+ dependencies = [
9
+ "caido-server-auth>=0.1.2",
10
+ "gql[aiohttp,websockets]>=3.5.0",
11
+ "pydantic>=2.11.0",
12
+ ]
13
+
14
+ [dependency-groups]
15
+ dev = ["pytest>=8.0.0", "pytest-asyncio>=0.24.0"]
16
+
17
+ [tool.pytest.ini_options]
18
+ asyncio_mode = "auto"
19
+ asyncio_default_fixture_loop_scope = "session"
20
+ testpaths = ["tests"]
21
+ # Run tests one at a time (no parallel workers); integration tests share one Caido instance
22
+ addopts = ["-p", "no:xdist"]
23
+
24
+ [build-system]
25
+ requires = ["uv_build>=0.9.8,<0.10.0"]
26
+ build-backend = "uv_build"
@@ -0,0 +1,191 @@
1
+ """Public package exports."""
2
+
3
+ from caido_sdk_client.auth import (
4
+ AuthCacheFile,
5
+ AuthManager,
6
+ AuthOptions,
7
+ BrowserAuthOptions,
8
+ CachedToken,
9
+ FileTokenCache,
10
+ PATAuthOptions,
11
+ TokenAuthOptions,
12
+ TokenCache,
13
+ TokenPair,
14
+ )
15
+ from caido_sdk_client.client import Client, ConnectOptions, Health, ReadyOptions
16
+ from caido_sdk_client.errors import (
17
+ BaseError,
18
+ InstanceNotReadyError,
19
+ NoViewerInResponseError,
20
+ UnsupportedViewerTypeError,
21
+ from_error,
22
+ )
23
+ from caido_sdk_client.graphql import GraphQLClient
24
+ from caido_sdk_client.logger import ConsoleLogger, Logger
25
+ from caido_sdk_client.rest import RestClient
26
+ from caido_sdk_client.sdks import (
27
+ EnvironmentInstance,
28
+ EnvironmentSDK,
29
+ FilterSDK,
30
+ FindingSDK,
31
+ FindingsListBuilder,
32
+ HostedFileSDK,
33
+ InstanceSDK,
34
+ PluginPackage,
35
+ PluginSDK,
36
+ RequestSDK,
37
+ RequestsListBuilder,
38
+ ScopeSDK,
39
+ UserSDK,
40
+ )
41
+ from caido_sdk_client.types import (
42
+ AISettings,
43
+ AnalyticsSettings,
44
+ AnthropicAISetting,
45
+ CloudUser,
46
+ CreateEnvironmentOptions,
47
+ CreateFilterPresetOptions,
48
+ CreateFindingOptions,
49
+ CreateScopeOptions,
50
+ Cursor,
51
+ CursorLike,
52
+ Environment,
53
+ EnvironmentVariable,
54
+ EnvironmentVariableKind,
55
+ FilterPreset,
56
+ Finding,
57
+ GoogleAISetting,
58
+ GuestUser,
59
+ HostedFile,
60
+ Httpql,
61
+ HttpqlLike,
62
+ Id,
63
+ IdLike,
64
+ InstallPluginPackageOptions,
65
+ InstallPluginPackageSourceFile,
66
+ InstallPluginPackageSourceManifest,
67
+ InstanceSettings,
68
+ OnboardingSettings,
69
+ OpenAIAISetting,
70
+ OpenRouterAISetting,
71
+ Plugin,
72
+ PluginBackend,
73
+ PluginFrontend,
74
+ PluginWorkflow,
75
+ Request,
76
+ RequestGetOptions,
77
+ RequestMetadata,
78
+ RequestResponseOpt,
79
+ Response,
80
+ Scope,
81
+ ScriptUser,
82
+ SetAISettingsInput,
83
+ SetAnalyticsSettingsInput,
84
+ SetInstanceSettingsInput,
85
+ SetOnboardingSettingsInput,
86
+ SubscriptionEntitlement,
87
+ SubscriptionPlan,
88
+ UpdateEnvironmentOptions,
89
+ UpdateFilterPresetOptions,
90
+ UpdateFindingOptions,
91
+ UpdateScopeOptions,
92
+ UploadHostedFileOptions,
93
+ User,
94
+ UserIdentity,
95
+ UserProfile,
96
+ UserSubscription,
97
+ )
98
+
99
+ __all__ = [
100
+ "AISettings",
101
+ "AnalyticsSettings",
102
+ "AnthropicAISetting",
103
+ "AuthCacheFile",
104
+ "AuthManager",
105
+ "AuthOptions",
106
+ "BaseError",
107
+ "BrowserAuthOptions",
108
+ "CachedToken",
109
+ "Client",
110
+ "CloudUser",
111
+ "ConnectOptions",
112
+ "ConsoleLogger",
113
+ "CreateEnvironmentOptions",
114
+ "CreateFilterPresetOptions",
115
+ "CreateFindingOptions",
116
+ "CreateScopeOptions",
117
+ "Cursor",
118
+ "CursorLike",
119
+ "Environment",
120
+ "EnvironmentInstance",
121
+ "EnvironmentSDK",
122
+ "EnvironmentVariable",
123
+ "EnvironmentVariableKind",
124
+ "FileTokenCache",
125
+ "FilterPreset",
126
+ "FilterSDK",
127
+ "Finding",
128
+ "HostedFile",
129
+ "HostedFileSDK",
130
+ "FindingSDK",
131
+ "FindingsListBuilder",
132
+ "GoogleAISetting",
133
+ "GraphQLClient",
134
+ "GuestUser",
135
+ "Health",
136
+ "InstallPluginPackageOptions",
137
+ "InstallPluginPackageSourceFile",
138
+ "InstallPluginPackageSourceManifest",
139
+ "Httpql",
140
+ "HttpqlLike",
141
+ "Id",
142
+ "IdLike",
143
+ "InstanceNotReadyError",
144
+ "InstanceSDK",
145
+ "InstanceSettings",
146
+ "Plugin",
147
+ "PluginBackend",
148
+ "PluginFrontend",
149
+ "PluginPackage",
150
+ "PluginSDK",
151
+ "PluginWorkflow",
152
+ "Logger",
153
+ "NoViewerInResponseError",
154
+ "OnboardingSettings",
155
+ "OpenAIAISetting",
156
+ "OpenRouterAISetting",
157
+ "PATAuthOptions",
158
+ "ReadyOptions",
159
+ "Request",
160
+ "RequestGetOptions",
161
+ "RequestMetadata",
162
+ "RequestResponseOpt",
163
+ "RequestSDK",
164
+ "RequestsListBuilder",
165
+ "Response",
166
+ "RestClient",
167
+ "Scope",
168
+ "ScopeSDK",
169
+ "ScriptUser",
170
+ "SetAISettingsInput",
171
+ "SetAnalyticsSettingsInput",
172
+ "SetInstanceSettingsInput",
173
+ "SetOnboardingSettingsInput",
174
+ "SubscriptionEntitlement",
175
+ "SubscriptionPlan",
176
+ "TokenAuthOptions",
177
+ "TokenCache",
178
+ "TokenPair",
179
+ "UnsupportedViewerTypeError",
180
+ "UpdateEnvironmentOptions",
181
+ "UploadHostedFileOptions",
182
+ "UpdateFilterPresetOptions",
183
+ "UpdateFindingOptions",
184
+ "UpdateScopeOptions",
185
+ "User",
186
+ "UserIdentity",
187
+ "UserProfile",
188
+ "UserSDK",
189
+ "UserSubscription",
190
+ "from_error",
191
+ ]
@@ -0,0 +1,27 @@
1
+ """Authentication module exports."""
2
+
3
+ from caido_sdk_client.auth.cache import CachedToken, FileTokenCache, TokenCache
4
+ from caido_sdk_client.auth.manager import AuthManager
5
+ from caido_sdk_client.auth.types import (
6
+ AuthCacheFile,
7
+ AuthCacheOptions,
8
+ AuthOptions,
9
+ BrowserAuthOptions,
10
+ PATAuthOptions,
11
+ TokenAuthOptions,
12
+ TokenPair,
13
+ )
14
+
15
+ __all__ = [
16
+ "AuthCacheFile",
17
+ "AuthCacheOptions",
18
+ "AuthManager",
19
+ "AuthOptions",
20
+ "BrowserAuthOptions",
21
+ "CachedToken",
22
+ "FileTokenCache",
23
+ "PATAuthOptions",
24
+ "TokenAuthOptions",
25
+ "TokenCache",
26
+ "TokenPair",
27
+ ]
@@ -0,0 +1,6 @@
1
+ """Token cache exports."""
2
+
3
+ from caido_sdk_client.auth.cache.file import FileTokenCache
4
+ from caido_sdk_client.auth.cache.types import CachedToken, TokenCache
5
+
6
+ __all__ = ["CachedToken", "FileTokenCache", "TokenCache"]
@@ -0,0 +1,71 @@
1
+ """File-based token cache."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+
9
+ from caido_sdk_client.auth.cache.types import CachedToken
10
+ from caido_sdk_client.logger import Logger
11
+
12
+
13
+ class FileTokenCache:
14
+ def __init__(
15
+ self,
16
+ *,
17
+ path: str | None = None,
18
+ logger: Logger | None = None,
19
+ ) -> None:
20
+ self._path = path or ".caido-token.json"
21
+ self._logger = logger
22
+
23
+ async def load(self) -> CachedToken | None:
24
+ try:
25
+ resolved = self._resolve_path()
26
+ with open(resolved) as f:
27
+ parsed = json.load(f)
28
+
29
+ access_token = parsed.get("access_token")
30
+ if access_token is None:
31
+ return None
32
+
33
+ return CachedToken(
34
+ access_token=access_token,
35
+ refresh_token=parsed.get("refresh_token"),
36
+ expires_at=parsed.get("expires_at"),
37
+ )
38
+ except Exception:
39
+ if self._logger is not None:
40
+ self._logger.debug("Failed to load cached token from file")
41
+ return None
42
+
43
+ async def save(self, token: CachedToken) -> None:
44
+ try:
45
+ resolved = self._resolve_path()
46
+ Path(resolved).parent.mkdir(parents=True, exist_ok=True)
47
+
48
+ data = {
49
+ "access_token": token.access_token,
50
+ "refresh_token": token.refresh_token,
51
+ "expires_at": token.expires_at,
52
+ }
53
+ fd = os.open(resolved, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
54
+ with os.fdopen(fd, "w") as f:
55
+ json.dump(data, f, indent=2)
56
+ except Exception:
57
+ if self._logger is not None:
58
+ self._logger.warn("Failed to save token cache to file")
59
+
60
+ async def clear(self) -> None:
61
+ try:
62
+ resolved = self._resolve_path()
63
+ os.unlink(resolved)
64
+ except Exception:
65
+ if self._logger is not None:
66
+ self._logger.warn("Failed to clear token cache file")
67
+
68
+ def _resolve_path(self) -> str:
69
+ if os.path.isabs(self._path):
70
+ return self._path
71
+ return os.path.join(os.getcwd(), self._path)
@@ -0,0 +1,19 @@
1
+ """Cache types for persisting authentication tokens."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Protocol
7
+
8
+
9
+ @dataclass(slots=True)
10
+ class CachedToken:
11
+ access_token: str
12
+ refresh_token: str | None = None
13
+ expires_at: str | None = None
14
+
15
+
16
+ class TokenCache(Protocol):
17
+ async def load(self) -> CachedToken | None: ...
18
+ async def save(self, token: CachedToken) -> None: ...
19
+ async def clear(self) -> None: ...
@@ -0,0 +1,196 @@
1
+ """Authentication manager."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+ from typing import Any
9
+
10
+ from caido_server_auth import (
11
+ AuthClient,
12
+ AuthClientOptions,
13
+ BrowserApprover,
14
+ PATApprover,
15
+ PATApproverOptions,
16
+ )
17
+
18
+ from caido_sdk_client.auth.cache.types import CachedToken, TokenCache
19
+ from caido_sdk_client.auth.types import (
20
+ AuthOptions,
21
+ BrowserAuthOptions,
22
+ TokenAuthOptions,
23
+ )
24
+ from caido_sdk_client.auth.utils import is_pat_auth, resolve_cache
25
+ from caido_sdk_client.errors.auth import TokenRefreshError
26
+ from caido_sdk_client.logger import Logger
27
+
28
+
29
+ @dataclass(slots=True)
30
+ class _TokenState:
31
+ access_token: str
32
+ refresh_token: str | None = None
33
+ expires_at: datetime | None = None
34
+
35
+
36
+ class AuthManager:
37
+ def __init__(
38
+ self,
39
+ instance_url: str,
40
+ logger: Logger,
41
+ auth_options: AuthOptions | None = None,
42
+ ) -> None:
43
+ self._instance_url = instance_url
44
+ self._logger = logger
45
+ self._auth_options = auth_options
46
+ self._cache: TokenCache | None = (
47
+ resolve_cache(auth_options.cache, logger)
48
+ if auth_options is not None and auth_options.cache is not None
49
+ else None
50
+ )
51
+
52
+ self._token_state: _TokenState | None = None
53
+ self._auth_client: Any = None
54
+ self._on_token_refresh_callbacks: set[Callable[[], None]] = set()
55
+
56
+ def get_access_token(self) -> str | None:
57
+ if self._token_state is None:
58
+ return None
59
+ return self._token_state.access_token
60
+
61
+ def can_refresh(self) -> bool:
62
+ return (
63
+ self._token_state is not None
64
+ and self._token_state.refresh_token is not None
65
+ )
66
+
67
+ def on_token_refresh(self, callback: Callable[[], None]) -> Callable[[], None]:
68
+ """Subscribe to token refresh events.
69
+
70
+ Returns an unsubscribe function.
71
+ """
72
+ self._on_token_refresh_callbacks.add(callback)
73
+
74
+ def unsubscribe() -> None:
75
+ self._on_token_refresh_callbacks.discard(callback)
76
+
77
+ return unsubscribe
78
+
79
+ def _notify_token_refresh(self) -> None:
80
+ for callback in self._on_token_refresh_callbacks:
81
+ try:
82
+ callback()
83
+ except Exception:
84
+ self._logger.warn("Error in token refresh callback")
85
+
86
+ async def authenticate(self) -> None:
87
+ auth = self._auth_options
88
+
89
+ cache = self._cache
90
+ if cache is not None:
91
+ self._logger.debug("Attempting to load cached token")
92
+ cached = await cache.load()
93
+ if cached is not None:
94
+ self._logger.info("Loaded token from cache")
95
+ self._token_state = _from_cached_token(cached)
96
+ self._notify_token_refresh()
97
+ return
98
+
99
+ if auth is not None and isinstance(auth, TokenAuthOptions):
100
+ self._logger.debug("Using provided token")
101
+ if isinstance(auth.token, str):
102
+ self._token_state = _TokenState(access_token=auth.token)
103
+ else:
104
+ self._token_state = _TokenState(
105
+ access_token=auth.token.access_token,
106
+ refresh_token=auth.token.refresh_token,
107
+ )
108
+ self._notify_token_refresh()
109
+ await self._maybe_cache_token()
110
+ return
111
+
112
+ self._logger.info("Starting authentication flow")
113
+ auth_client = self._get_or_create_auth_client()
114
+ token = await auth_client.start_authentication_flow()
115
+ self._apply_auth_token(token)
116
+ self._logger.info("Authentication flow completed")
117
+ await self._maybe_cache_token()
118
+
119
+ async def refresh(self) -> None:
120
+ refresh_token = self._token_state.refresh_token if self._token_state else None
121
+ if refresh_token is None:
122
+ raise TokenRefreshError()
123
+
124
+ self._logger.debug("Refreshing access token")
125
+ auth_client = self._get_or_create_auth_client()
126
+ token = await auth_client.refresh_token(refresh_token)
127
+ self._apply_auth_token(token)
128
+ self._logger.info("Access token refreshed")
129
+ await self._maybe_cache_token()
130
+ self._notify_token_refresh()
131
+
132
+ def _get_or_create_auth_client(self) -> Any:
133
+ if self._auth_client is None:
134
+ approver = self._create_approver()
135
+ self._auth_client = AuthClient(
136
+ AuthClientOptions(
137
+ instance_url=self._instance_url,
138
+ approver=approver,
139
+ )
140
+ )
141
+ return self._auth_client
142
+
143
+ def _create_approver(self) -> Any:
144
+ auth = self._auth_options
145
+
146
+ if auth is not None and is_pat_auth(auth):
147
+ return PATApprover(PATApproverOptions(pat=auth.pat)) # type: ignore[union-attr]
148
+
149
+ logger = self._logger
150
+ browser_auth = auth if isinstance(auth, BrowserAuthOptions) else None
151
+ on_request = (
152
+ browser_auth.on_request
153
+ if browser_auth is not None and browser_auth.on_request is not None
154
+ else lambda request: logger.info(
155
+ f"Please visit the following URL to authenticate: {request.verification_url}"
156
+ )
157
+ )
158
+ return BrowserApprover(on_request)
159
+
160
+ def _apply_auth_token(self, token: Any) -> None:
161
+ self._token_state = _TokenState(
162
+ access_token=token.access_token,
163
+ refresh_token=token.refresh_token,
164
+ expires_at=token.expires_at,
165
+ )
166
+ self._notify_token_refresh()
167
+
168
+ async def _maybe_cache_token(self) -> None:
169
+ cache = self._cache
170
+ token_state = self._token_state
171
+ if cache is None or token_state is None:
172
+ return
173
+ self._logger.debug("Saving token to cache")
174
+ await cache.save(_to_cached_token(token_state))
175
+
176
+
177
+ def _to_cached_token(state: _TokenState) -> CachedToken:
178
+ return CachedToken(
179
+ access_token=state.access_token,
180
+ refresh_token=state.refresh_token,
181
+ expires_at=state.expires_at.isoformat()
182
+ if state.expires_at is not None
183
+ else None,
184
+ )
185
+
186
+
187
+ def _from_cached_token(cached: CachedToken) -> _TokenState:
188
+ return _TokenState(
189
+ access_token=cached.access_token,
190
+ refresh_token=cached.refresh_token,
191
+ expires_at=(
192
+ datetime.fromisoformat(cached.expires_at)
193
+ if cached.expires_at is not None
194
+ else None
195
+ ),
196
+ )
@@ -0,0 +1,61 @@
1
+ """Authentication option types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass
7
+ from typing import Any, Union
8
+
9
+ from caido_sdk_client.auth.cache.types import TokenCache
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class AuthCacheFile:
14
+ """File-based cache configuration. Path is relative to cwd or absolute."""
15
+
16
+ file: str
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class AuthCacheLocalStorage:
21
+ """LocalStorage-based cache configuration (browser only)."""
22
+
23
+ localstorage: str
24
+
25
+
26
+ AuthCacheOptions = Union[AuthCacheFile, AuthCacheLocalStorage, TokenCache]
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class TokenPair:
31
+ """A pair of access and refresh tokens."""
32
+
33
+ access_token: str
34
+ refresh_token: str | None = None
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class PATAuthOptions:
39
+ """Authenticate using a Personal Access Token (PAT)."""
40
+
41
+ pat: str
42
+ cache: AuthCacheOptions | None = None
43
+
44
+
45
+ @dataclass(frozen=True, slots=True)
46
+ class TokenAuthOptions:
47
+ """Authenticate with a direct access token or token pair."""
48
+
49
+ token: str | TokenPair
50
+ cache: AuthCacheOptions | None = None
51
+
52
+
53
+ @dataclass(frozen=True, slots=True)
54
+ class BrowserAuthOptions:
55
+ """Authenticate via browser-based device code flow."""
56
+
57
+ on_request: Callable[[Any], None] | None = None
58
+ cache: AuthCacheOptions | None = None
59
+
60
+
61
+ AuthOptions = Union[PATAuthOptions, TokenAuthOptions, BrowserAuthOptions]
@@ -0,0 +1,31 @@
1
+ """Authentication utility functions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from caido_sdk_client.auth.cache.file import FileTokenCache
6
+ from caido_sdk_client.auth.cache.types import TokenCache
7
+ from caido_sdk_client.auth.types import (
8
+ AuthCacheFile,
9
+ AuthCacheLocalStorage,
10
+ AuthCacheOptions,
11
+ AuthOptions,
12
+ PATAuthOptions,
13
+ TokenAuthOptions,
14
+ )
15
+ from caido_sdk_client.logger import Logger
16
+
17
+
18
+ def is_pat_auth(auth: AuthOptions) -> bool:
19
+ return isinstance(auth, PATAuthOptions)
20
+
21
+
22
+ def is_token_auth(auth: AuthOptions) -> bool:
23
+ return isinstance(auth, TokenAuthOptions)
24
+
25
+
26
+ def resolve_cache(options: AuthCacheOptions, logger: Logger) -> TokenCache:
27
+ if isinstance(options, AuthCacheFile):
28
+ return FileTokenCache(path=options.file, logger=logger)
29
+ if isinstance(options, AuthCacheLocalStorage):
30
+ raise NotImplementedError("LocalStorage cache is not supported in Python")
31
+ return options