viio-sync-api-sdk 0.1.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.
- viio_sync_api/__init__.py +27 -0
- viio_sync_api/_auth.py +88 -0
- viio_sync_api/_client.py +192 -0
- viio_sync_api/_endpoint.py +61 -0
- viio_sync_api/_errors.py +32 -0
- viio_sync_api/_options.py +35 -0
- viio_sync_api/_transport.py +154 -0
- viio_sync_api/py.typed +0 -0
- viio_sync_api/v1/__init__.py +10 -0
- viio_sync_api/v1/anthropic_pb2.py +62 -0
- viio_sync_api/v1/anthropic_pb2.pyi +188 -0
- viio_sync_api/v1/anthropic_pb2_grpc.py +24 -0
- viio_sync_api/v1/bamboo_hr_pb2.py +38 -0
- viio_sync_api/v1/bamboo_hr_pb2.pyi +47 -0
- viio_sync_api/v1/bamboo_hr_pb2_grpc.py +24 -0
- viio_sync_api/v1/contracts_pb2.py +160 -0
- viio_sync_api/v1/contracts_pb2.pyi +510 -0
- viio_sync_api/v1/contracts_pb2_grpc.py +226 -0
- viio_sync_api/v1/generic_pb2.py +42 -0
- viio_sync_api/v1/generic_pb2.pyi +63 -0
- viio_sync_api/v1/generic_pb2_grpc.py +24 -0
- viio_sync_api/v1/google_pb2.py +46 -0
- viio_sync_api/v1/google_pb2.pyi +84 -0
- viio_sync_api/v1/google_pb2_grpc.py +24 -0
- viio_sync_api/v1/microsoft_pb2.py +40 -0
- viio_sync_api/v1/microsoft_pb2.pyi +51 -0
- viio_sync_api/v1/microsoft_pb2_grpc.py +24 -0
- viio_sync_api/v1/okta_pb2.py +40 -0
- viio_sync_api/v1/okta_pb2.pyi +47 -0
- viio_sync_api/v1/okta_pb2_grpc.py +24 -0
- viio_sync_api_sdk-0.1.0.dist-info/METADATA +145 -0
- viio_sync_api_sdk-0.1.0.dist-info/RECORD +34 -0
- viio_sync_api_sdk-0.1.0.dist-info/WHEEL +4 -0
- viio_sync_api_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Async Python SDK for the Viio Sync API."""
|
|
2
|
+
|
|
3
|
+
from ._auth import BearerTokenProvider, SyncApiAuthentication
|
|
4
|
+
from ._client import SyncApiBatch, SyncApiClient
|
|
5
|
+
from ._errors import (
|
|
6
|
+
SyncApiAuthenticationError,
|
|
7
|
+
SyncApiBatchAlreadyInProgressError,
|
|
8
|
+
SyncApiBatchStateError,
|
|
9
|
+
SyncApiClosedError,
|
|
10
|
+
SyncApiError,
|
|
11
|
+
SyncApiProtocolError,
|
|
12
|
+
)
|
|
13
|
+
from ._options import SyncApiClientOptions
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"BearerTokenProvider",
|
|
17
|
+
"SyncApiAuthentication",
|
|
18
|
+
"SyncApiAuthenticationError",
|
|
19
|
+
"SyncApiBatch",
|
|
20
|
+
"SyncApiBatchAlreadyInProgressError",
|
|
21
|
+
"SyncApiBatchStateError",
|
|
22
|
+
"SyncApiClient",
|
|
23
|
+
"SyncApiClientOptions",
|
|
24
|
+
"SyncApiClosedError",
|
|
25
|
+
"SyncApiError",
|
|
26
|
+
"SyncApiProtocolError",
|
|
27
|
+
]
|
viio_sync_api/_auth.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Authentication modes shared by high-level and generated client calls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import inspect
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from collections.abc import Awaitable, Callable
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Protocol
|
|
11
|
+
|
|
12
|
+
from ._errors import SyncApiAuthenticationError
|
|
13
|
+
|
|
14
|
+
BearerTokenProvider = Callable[[], Awaitable[str]]
|
|
15
|
+
AuthorizationMetadata = tuple[str, str]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _AuthorizationProvider(Protocol):
|
|
19
|
+
async def get_authorization(self) -> AuthorizationMetadata: ...
|
|
20
|
+
|
|
21
|
+
async def close(self) -> None: ...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SyncApiAuthentication(ABC):
|
|
25
|
+
"""Defines exactly one authentication mode for a client."""
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def api_key(api_key: str) -> SyncApiAuthentication:
|
|
29
|
+
"""Use a fixed API key sent with the ``ApiKey`` authorization scheme."""
|
|
30
|
+
if not isinstance(api_key, str) or not api_key.strip():
|
|
31
|
+
raise ValueError("api_key cannot be empty")
|
|
32
|
+
return _ProviderAuthentication("ApiKey", lambda: _completed(api_key))
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def bearer_token(token_provider: BearerTokenProvider) -> SyncApiAuthentication:
|
|
36
|
+
"""Use an asynchronous bearer provider owned by a Viio-managed caller."""
|
|
37
|
+
if not callable(token_provider):
|
|
38
|
+
raise TypeError("token_provider must be callable")
|
|
39
|
+
return _ProviderAuthentication("Bearer", token_provider)
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
def _create_provider(self) -> _AuthorizationProvider:
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def _completed(value: str) -> str:
|
|
47
|
+
return value
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True, slots=True, repr=False)
|
|
51
|
+
class _ProviderAuthentication(SyncApiAuthentication):
|
|
52
|
+
scheme: str
|
|
53
|
+
value_provider: BearerTokenProvider
|
|
54
|
+
|
|
55
|
+
def _create_provider(self) -> _AuthorizationProvider:
|
|
56
|
+
return _DelegateAuthorizationProvider(self.scheme, self.value_provider)
|
|
57
|
+
|
|
58
|
+
def __repr__(self) -> str:
|
|
59
|
+
return f"SyncApiAuthentication({self.scheme})"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(slots=True)
|
|
63
|
+
class _DelegateAuthorizationProvider:
|
|
64
|
+
scheme: str
|
|
65
|
+
value_provider: BearerTokenProvider
|
|
66
|
+
|
|
67
|
+
async def get_authorization(self) -> AuthorizationMetadata:
|
|
68
|
+
try:
|
|
69
|
+
value_awaitable = self.value_provider()
|
|
70
|
+
except Exception:
|
|
71
|
+
raise SyncApiAuthenticationError("The authentication provider failed to provide a credential") from None
|
|
72
|
+
|
|
73
|
+
if not inspect.isawaitable(value_awaitable):
|
|
74
|
+
raise SyncApiAuthenticationError("The authentication provider returned no awaitable credential")
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
value = await value_awaitable
|
|
78
|
+
except asyncio.CancelledError:
|
|
79
|
+
raise
|
|
80
|
+
except Exception:
|
|
81
|
+
raise SyncApiAuthenticationError("The authentication provider failed to provide a credential") from None
|
|
82
|
+
|
|
83
|
+
if not isinstance(value, str) or not value.strip():
|
|
84
|
+
raise SyncApiAuthenticationError("The authentication provider returned an empty credential")
|
|
85
|
+
return "authorization", f"{self.scheme} {value}"
|
|
86
|
+
|
|
87
|
+
async def close(self) -> None:
|
|
88
|
+
return None
|
viio_sync_api/_client.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""High-level AsyncIO client and explicit batch lifecycle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from types import TracebackType
|
|
8
|
+
|
|
9
|
+
from ._errors import (
|
|
10
|
+
SyncApiBatchAlreadyInProgressError,
|
|
11
|
+
SyncApiBatchStateError,
|
|
12
|
+
SyncApiClosedError,
|
|
13
|
+
SyncApiProtocolError,
|
|
14
|
+
)
|
|
15
|
+
from ._options import SyncApiClientOptions
|
|
16
|
+
from ._transport import _GrpcSyncApiTransport, _SyncApiTransport
|
|
17
|
+
from .v1 import (
|
|
18
|
+
SYNC_ERROR_CODE_SYNC_ALREADY_IN_PROGRESS,
|
|
19
|
+
AbortSyncBatchRequest,
|
|
20
|
+
CompleteSyncBatchRequest,
|
|
21
|
+
StartSyncBatchRequest,
|
|
22
|
+
SyncRecordsRequest,
|
|
23
|
+
SyncServiceStub,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_TYPED_WRAPPERS = (
|
|
27
|
+
"accounts",
|
|
28
|
+
"licenses",
|
|
29
|
+
"plans",
|
|
30
|
+
"usage",
|
|
31
|
+
"employees",
|
|
32
|
+
"groups",
|
|
33
|
+
"group_members",
|
|
34
|
+
"externally_discovered_usage",
|
|
35
|
+
"devices",
|
|
36
|
+
"audit_logs",
|
|
37
|
+
"ai_usage",
|
|
38
|
+
"ai_costs",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SyncApiClient:
|
|
43
|
+
"""Supported AsyncIO client for authenticated Sync API batch operations."""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
options: SyncApiClientOptions,
|
|
48
|
+
*,
|
|
49
|
+
_transport: _SyncApiTransport | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
if not isinstance(options, SyncApiClientOptions):
|
|
52
|
+
raise TypeError("options must be a SyncApiClientOptions")
|
|
53
|
+
options.validate()
|
|
54
|
+
self._transport = _transport or _GrpcSyncApiTransport(options)
|
|
55
|
+
self._closed = False
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def grpc_client(self) -> SyncServiceStub:
|
|
59
|
+
"""The authenticated generated v1 stub for advanced calls."""
|
|
60
|
+
self._ensure_open()
|
|
61
|
+
return self._transport.grpc_client
|
|
62
|
+
|
|
63
|
+
async def start_batch(
|
|
64
|
+
self,
|
|
65
|
+
direct_integration_installation_id: str,
|
|
66
|
+
) -> SyncApiBatch:
|
|
67
|
+
"""Start a server-owned batch and return its lifecycle-aware handle."""
|
|
68
|
+
self._ensure_open()
|
|
69
|
+
if not isinstance(direct_integration_installation_id, str) or not direct_integration_installation_id.strip():
|
|
70
|
+
raise ValueError("direct_integration_installation_id cannot be empty")
|
|
71
|
+
|
|
72
|
+
response = await self._transport.start(
|
|
73
|
+
StartSyncBatchRequest(direct_integration_installation_id=direct_integration_installation_id)
|
|
74
|
+
)
|
|
75
|
+
if not response.success:
|
|
76
|
+
if response.error_code == SYNC_ERROR_CODE_SYNC_ALREADY_IN_PROGRESS:
|
|
77
|
+
raise SyncApiBatchAlreadyInProgressError(response.message)
|
|
78
|
+
raise SyncApiProtocolError("StartBatch", response.message)
|
|
79
|
+
if not response.batch_id.strip():
|
|
80
|
+
raise SyncApiProtocolError(
|
|
81
|
+
"StartBatch",
|
|
82
|
+
"The server returned an empty batch ID.",
|
|
83
|
+
)
|
|
84
|
+
return SyncApiBatch(self, response.batch_id)
|
|
85
|
+
|
|
86
|
+
async def _write(self, batch_id: str, request: SyncRecordsRequest) -> None:
|
|
87
|
+
self._ensure_open()
|
|
88
|
+
if not isinstance(request, SyncRecordsRequest):
|
|
89
|
+
raise TypeError("request must be a SyncRecordsRequest")
|
|
90
|
+
wrapper_count = sum(request.HasField(field) for field in _TYPED_WRAPPERS)
|
|
91
|
+
if wrapper_count != 1:
|
|
92
|
+
raise ValueError("A write request must contain exactly one typed records wrapper")
|
|
93
|
+
|
|
94
|
+
request_with_batch_id = SyncRecordsRequest()
|
|
95
|
+
request_with_batch_id.CopyFrom(request)
|
|
96
|
+
request_with_batch_id.batch_id = batch_id
|
|
97
|
+
response = await self._transport.write(request_with_batch_id)
|
|
98
|
+
if not response.success:
|
|
99
|
+
raise SyncApiProtocolError("Write", response.message)
|
|
100
|
+
|
|
101
|
+
async def _complete(self, batch_id: str) -> None:
|
|
102
|
+
self._ensure_open()
|
|
103
|
+
response = await self._transport.complete(CompleteSyncBatchRequest(batch_id=batch_id))
|
|
104
|
+
if not response.success:
|
|
105
|
+
raise SyncApiProtocolError("CompleteBatch", response.message)
|
|
106
|
+
|
|
107
|
+
async def _abort(self, batch_id: str) -> None:
|
|
108
|
+
self._ensure_open()
|
|
109
|
+
response = await self._transport.abort(AbortSyncBatchRequest(batch_id=batch_id))
|
|
110
|
+
if not response.success:
|
|
111
|
+
raise SyncApiProtocolError("AbortBatch", response.message)
|
|
112
|
+
|
|
113
|
+
def _ensure_open(self) -> None:
|
|
114
|
+
if self._closed:
|
|
115
|
+
raise SyncApiClosedError("The Sync API client is closed")
|
|
116
|
+
|
|
117
|
+
async def close(self) -> None:
|
|
118
|
+
"""Release SDK-owned channel and authentication resources."""
|
|
119
|
+
if self._closed:
|
|
120
|
+
return
|
|
121
|
+
self._closed = True
|
|
122
|
+
await self._transport.close()
|
|
123
|
+
|
|
124
|
+
async def __aenter__(self) -> SyncApiClient:
|
|
125
|
+
self._ensure_open()
|
|
126
|
+
return self
|
|
127
|
+
|
|
128
|
+
async def __aexit__(
|
|
129
|
+
self,
|
|
130
|
+
exception_type: type[BaseException] | None,
|
|
131
|
+
exception: BaseException | None,
|
|
132
|
+
traceback: TracebackType | None,
|
|
133
|
+
) -> None:
|
|
134
|
+
del exception_type, exception, traceback
|
|
135
|
+
await self.close()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class SyncApiBatch:
|
|
139
|
+
"""One explicit Sync API batch with serialized local lifecycle checks."""
|
|
140
|
+
|
|
141
|
+
def __init__(self, client: SyncApiClient, batch_id: str) -> None:
|
|
142
|
+
self._client = client
|
|
143
|
+
self._batch_id = batch_id
|
|
144
|
+
self._operation_lock = asyncio.Lock()
|
|
145
|
+
self._state = _BatchState.ACTIVE
|
|
146
|
+
self._closed = False
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def batch_id(self) -> str:
|
|
150
|
+
"""The server-generated batch identifier."""
|
|
151
|
+
return self._batch_id
|
|
152
|
+
|
|
153
|
+
async def write(self, request: SyncRecordsRequest) -> None:
|
|
154
|
+
"""Write one typed wrapper; concurrent batch operations are serialized."""
|
|
155
|
+
self._ensure_open()
|
|
156
|
+
async with self._operation_lock:
|
|
157
|
+
self._ensure_active()
|
|
158
|
+
await self._client._write(self._batch_id, request)
|
|
159
|
+
|
|
160
|
+
async def complete(self) -> None:
|
|
161
|
+
"""Complete the batch and reject future operations after success."""
|
|
162
|
+
self._ensure_open()
|
|
163
|
+
async with self._operation_lock:
|
|
164
|
+
self._ensure_active()
|
|
165
|
+
await self._client._complete(self._batch_id)
|
|
166
|
+
self._state = _BatchState.COMPLETED
|
|
167
|
+
|
|
168
|
+
async def abort(self) -> None:
|
|
169
|
+
"""Abort the batch and reject future operations after success."""
|
|
170
|
+
self._ensure_open()
|
|
171
|
+
async with self._operation_lock:
|
|
172
|
+
self._ensure_active()
|
|
173
|
+
await self._client._abort(self._batch_id)
|
|
174
|
+
self._state = _BatchState.ABORTED
|
|
175
|
+
|
|
176
|
+
def close(self) -> None:
|
|
177
|
+
"""Release local state without completing or aborting the remote batch."""
|
|
178
|
+
self._closed = True
|
|
179
|
+
|
|
180
|
+
def _ensure_open(self) -> None:
|
|
181
|
+
if self._closed:
|
|
182
|
+
raise SyncApiClosedError("The Sync API batch is closed")
|
|
183
|
+
|
|
184
|
+
def _ensure_active(self) -> None:
|
|
185
|
+
if self._state is not _BatchState.ACTIVE:
|
|
186
|
+
raise SyncApiBatchStateError(f"The batch has already been {self._state.value}")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class _BatchState(Enum):
|
|
190
|
+
ACTIVE = "active"
|
|
191
|
+
COMPLETED = "completed"
|
|
192
|
+
ABORTED = "aborted"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Validate a Sync API URL and translate it to a Python gRPC target."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ipaddress
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from urllib.parse import SplitResult, urlsplit
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class ParsedEndpoint:
|
|
12
|
+
secure: bool
|
|
13
|
+
target: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_endpoint(value: str) -> ParsedEndpoint:
|
|
17
|
+
if not isinstance(value, str) or not value.strip():
|
|
18
|
+
raise ValueError("endpoint cannot be empty")
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
parsed = urlsplit(value)
|
|
22
|
+
port = parsed.port
|
|
23
|
+
except ValueError as error:
|
|
24
|
+
raise ValueError("endpoint must be a valid absolute URL") from error
|
|
25
|
+
|
|
26
|
+
if (
|
|
27
|
+
parsed.scheme not in {"https", "http"}
|
|
28
|
+
or not parsed.hostname
|
|
29
|
+
or parsed.username is not None
|
|
30
|
+
or parsed.password is not None
|
|
31
|
+
or parsed.query
|
|
32
|
+
or parsed.fragment
|
|
33
|
+
):
|
|
34
|
+
raise ValueError(
|
|
35
|
+
"endpoint must be an absolute HTTPS URL, or an HTTP loopback URL, without embedded credentials"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if parsed.path not in {"", "/"}:
|
|
39
|
+
raise ValueError("endpoint cannot contain a path")
|
|
40
|
+
|
|
41
|
+
secure = parsed.scheme == "https"
|
|
42
|
+
if not secure and not _is_loopback(parsed):
|
|
43
|
+
raise ValueError("endpoint must use HTTPS unless it targets a loopback host")
|
|
44
|
+
|
|
45
|
+
endpoint_port = port or (443 if secure else 80)
|
|
46
|
+
host = parsed.hostname
|
|
47
|
+
assert host is not None
|
|
48
|
+
target_host = f"[{host}]" if ":" in host else host
|
|
49
|
+
return ParsedEndpoint(secure=secure, target=f"{target_host}:{endpoint_port}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _is_loopback(parsed: SplitResult) -> bool:
|
|
53
|
+
hostname = parsed.hostname
|
|
54
|
+
if hostname is None:
|
|
55
|
+
return False
|
|
56
|
+
if hostname.lower() == "localhost":
|
|
57
|
+
return True
|
|
58
|
+
try:
|
|
59
|
+
return ipaddress.ip_address(hostname).is_loopback
|
|
60
|
+
except ValueError:
|
|
61
|
+
return False
|
viio_sync_api/_errors.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Public SDK-specific exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SyncApiError(Exception):
|
|
5
|
+
"""Base class for SDK-specific failures."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SyncApiAuthenticationError(SyncApiError):
|
|
9
|
+
"""Authentication could not produce a usable credential."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SyncApiProtocolError(SyncApiError):
|
|
13
|
+
"""The Sync API returned a valid response that reports failure."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, operation: str, message: str) -> None:
|
|
16
|
+
super().__init__(f"Sync API operation {operation!r} failed: {message}")
|
|
17
|
+
self.operation = operation
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SyncApiBatchAlreadyInProgressError(SyncApiProtocolError):
|
|
21
|
+
"""A batch could not start because another sync is already in progress."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, message: str) -> None:
|
|
24
|
+
super().__init__("StartBatch", message)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SyncApiBatchStateError(SyncApiError):
|
|
28
|
+
"""An operation is invalid for the batch's current local state."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SyncApiClosedError(SyncApiError):
|
|
32
|
+
"""An operation was attempted on a closed SDK resource."""
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Sync API client options."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from ._auth import SyncApiAuthentication
|
|
8
|
+
from ._endpoint import ParsedEndpoint, parse_endpoint
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(slots=True)
|
|
12
|
+
class SyncApiClientOptions:
|
|
13
|
+
"""Configures an authenticated AsyncIO Sync API client."""
|
|
14
|
+
|
|
15
|
+
endpoint: str
|
|
16
|
+
authentication: SyncApiAuthentication
|
|
17
|
+
# Maximum response-message size this client accepts. None uses gRPC's default.
|
|
18
|
+
max_receive_message_size: int | None = 16 * 1024 * 1024
|
|
19
|
+
# Maximum request-message size this client sends. None uses gRPC's default.
|
|
20
|
+
max_send_message_size: int | None = None
|
|
21
|
+
timeout: float = 100.0
|
|
22
|
+
|
|
23
|
+
def validate(self) -> ParsedEndpoint:
|
|
24
|
+
parsed = parse_endpoint(self.endpoint)
|
|
25
|
+
if not isinstance(self.authentication, SyncApiAuthentication):
|
|
26
|
+
raise TypeError("authentication must be a SyncApiAuthentication")
|
|
27
|
+
if self.max_receive_message_size is not None and self.max_receive_message_size <= 0:
|
|
28
|
+
raise ValueError("max_receive_message_size must be positive")
|
|
29
|
+
if self.max_send_message_size is not None and self.max_send_message_size <= 0:
|
|
30
|
+
raise ValueError("max_send_message_size must be positive")
|
|
31
|
+
if self.timeout <= 0:
|
|
32
|
+
raise ValueError("timeout must be positive and finite")
|
|
33
|
+
if self.timeout == float("inf"):
|
|
34
|
+
raise ValueError("timeout must be positive and finite")
|
|
35
|
+
return parsed
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Authenticated gRPC AsyncIO transport."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
6
|
+
from typing import Any, Protocol, cast
|
|
7
|
+
|
|
8
|
+
import grpc
|
|
9
|
+
|
|
10
|
+
from ._auth import _AuthorizationProvider
|
|
11
|
+
from ._options import SyncApiClientOptions
|
|
12
|
+
from .v1 import (
|
|
13
|
+
AbortSyncBatchRequest,
|
|
14
|
+
AbortSyncBatchResponse,
|
|
15
|
+
CompleteSyncBatchRequest,
|
|
16
|
+
CompleteSyncBatchResponse,
|
|
17
|
+
StartSyncBatchRequest,
|
|
18
|
+
StartSyncBatchResponse,
|
|
19
|
+
SyncRecordsRequest,
|
|
20
|
+
SyncRecordsResponse,
|
|
21
|
+
SyncServiceStub,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _SyncApiTransport(Protocol):
|
|
26
|
+
@property
|
|
27
|
+
def grpc_client(self) -> SyncServiceStub: ...
|
|
28
|
+
|
|
29
|
+
async def start(self, request: StartSyncBatchRequest) -> StartSyncBatchResponse: ...
|
|
30
|
+
|
|
31
|
+
async def write(self, request: SyncRecordsRequest) -> SyncRecordsResponse: ...
|
|
32
|
+
|
|
33
|
+
async def complete(self, request: CompleteSyncBatchRequest) -> CompleteSyncBatchResponse: ...
|
|
34
|
+
|
|
35
|
+
async def abort(self, request: AbortSyncBatchRequest) -> AbortSyncBatchResponse: ...
|
|
36
|
+
|
|
37
|
+
async def close(self) -> None: ...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _AuthenticationInterceptor:
|
|
41
|
+
def __init__(self, authorization_provider: _AuthorizationProvider) -> None:
|
|
42
|
+
self._authorization_provider = authorization_provider
|
|
43
|
+
|
|
44
|
+
async def _authenticated_details(
|
|
45
|
+
self,
|
|
46
|
+
details: grpc.aio.ClientCallDetails,
|
|
47
|
+
) -> grpc.aio.ClientCallDetails:
|
|
48
|
+
authorization = await self._authorization_provider.get_authorization()
|
|
49
|
+
metadata = grpc.aio.Metadata(*(item for item in (details.metadata or ()) if item[0].lower() != "authorization"))
|
|
50
|
+
metadata.add(*authorization)
|
|
51
|
+
return grpc.aio.ClientCallDetails(
|
|
52
|
+
method=details.method,
|
|
53
|
+
timeout=details.timeout,
|
|
54
|
+
metadata=metadata,
|
|
55
|
+
credentials=details.credentials,
|
|
56
|
+
wait_for_ready=details.wait_for_ready,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class _UnaryUnaryAuthenticationInterceptor(
|
|
61
|
+
_AuthenticationInterceptor,
|
|
62
|
+
grpc.aio.UnaryUnaryClientInterceptor,
|
|
63
|
+
):
|
|
64
|
+
async def intercept_unary_unary(
|
|
65
|
+
self,
|
|
66
|
+
continuation: Callable[[grpc.aio.ClientCallDetails, Any], Awaitable[Any]],
|
|
67
|
+
client_call_details: grpc.aio.ClientCallDetails,
|
|
68
|
+
request: Any,
|
|
69
|
+
) -> Any:
|
|
70
|
+
return await continuation(await self._authenticated_details(client_call_details), request)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class _StreamUnaryAuthenticationInterceptor(
|
|
74
|
+
_AuthenticationInterceptor,
|
|
75
|
+
grpc.aio.StreamUnaryClientInterceptor,
|
|
76
|
+
):
|
|
77
|
+
async def intercept_stream_unary(
|
|
78
|
+
self,
|
|
79
|
+
continuation: Callable[[grpc.aio.ClientCallDetails, Any], Awaitable[Any]],
|
|
80
|
+
client_call_details: grpc.aio.ClientCallDetails,
|
|
81
|
+
request_iterator: Any,
|
|
82
|
+
) -> Any:
|
|
83
|
+
return await continuation(await self._authenticated_details(client_call_details), request_iterator)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class _GrpcSyncApiTransport:
|
|
87
|
+
def __init__(self, options: SyncApiClientOptions) -> None:
|
|
88
|
+
endpoint = options.validate()
|
|
89
|
+
self._timeout = options.timeout
|
|
90
|
+
self._authorization_provider = options.authentication._create_provider()
|
|
91
|
+
interceptors: list[grpc.aio.ClientInterceptor] = [
|
|
92
|
+
_UnaryUnaryAuthenticationInterceptor(self._authorization_provider),
|
|
93
|
+
_StreamUnaryAuthenticationInterceptor(self._authorization_provider),
|
|
94
|
+
]
|
|
95
|
+
channel_options: list[tuple[str, Any]] = []
|
|
96
|
+
if options.max_receive_message_size is not None:
|
|
97
|
+
channel_options.append(("grpc.max_receive_message_length", options.max_receive_message_size))
|
|
98
|
+
if options.max_send_message_size is not None:
|
|
99
|
+
channel_options.append(("grpc.max_send_message_length", options.max_send_message_size))
|
|
100
|
+
|
|
101
|
+
if endpoint.secure:
|
|
102
|
+
credentials = grpc.ssl_channel_credentials()
|
|
103
|
+
self._channel = grpc.aio.secure_channel(
|
|
104
|
+
endpoint.target,
|
|
105
|
+
credentials,
|
|
106
|
+
options=channel_options,
|
|
107
|
+
interceptors=interceptors,
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
self._channel = grpc.aio.insecure_channel(
|
|
111
|
+
endpoint.target,
|
|
112
|
+
options=channel_options,
|
|
113
|
+
interceptors=interceptors,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
self._grpc_client = SyncServiceStub(self._channel) # type: ignore[no-untyped-call]
|
|
117
|
+
self._closed = False
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def grpc_client(self) -> SyncServiceStub:
|
|
121
|
+
return self._grpc_client
|
|
122
|
+
|
|
123
|
+
async def start(self, request: StartSyncBatchRequest) -> StartSyncBatchResponse:
|
|
124
|
+
response = await self._grpc_client.StartSyncBatch(request, timeout=self._timeout)
|
|
125
|
+
return cast(StartSyncBatchResponse, response)
|
|
126
|
+
|
|
127
|
+
async def write(self, request: SyncRecordsRequest) -> SyncRecordsResponse:
|
|
128
|
+
response = await self._grpc_client.SyncRecords(
|
|
129
|
+
_single_request(request),
|
|
130
|
+
timeout=self._timeout,
|
|
131
|
+
)
|
|
132
|
+
return cast(SyncRecordsResponse, response)
|
|
133
|
+
|
|
134
|
+
async def complete(
|
|
135
|
+
self,
|
|
136
|
+
request: CompleteSyncBatchRequest,
|
|
137
|
+
) -> CompleteSyncBatchResponse:
|
|
138
|
+
response = await self._grpc_client.CompleteSyncBatch(request, timeout=self._timeout)
|
|
139
|
+
return cast(CompleteSyncBatchResponse, response)
|
|
140
|
+
|
|
141
|
+
async def abort(self, request: AbortSyncBatchRequest) -> AbortSyncBatchResponse:
|
|
142
|
+
response = await self._grpc_client.AbortSyncBatch(request, timeout=self._timeout)
|
|
143
|
+
return cast(AbortSyncBatchResponse, response)
|
|
144
|
+
|
|
145
|
+
async def close(self) -> None:
|
|
146
|
+
if self._closed:
|
|
147
|
+
return
|
|
148
|
+
self._closed = True
|
|
149
|
+
await self._channel.close()
|
|
150
|
+
await self._authorization_provider.close()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
async def _single_request(request: SyncRecordsRequest) -> AsyncIterator[SyncRecordsRequest]:
|
|
154
|
+
yield request
|
viio_sync_api/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Generated ``viio.sync.v1`` protobuf messages and gRPC service types."""
|
|
2
|
+
|
|
3
|
+
from .anthropic_pb2 import * # noqa: F403
|
|
4
|
+
from .bamboo_hr_pb2 import * # noqa: F403
|
|
5
|
+
from .contracts_pb2 import * # noqa: F403
|
|
6
|
+
from .contracts_pb2_grpc import SyncServiceStub as SyncServiceStub
|
|
7
|
+
from .generic_pb2 import * # noqa: F403
|
|
8
|
+
from .google_pb2 import * # noqa: F403
|
|
9
|
+
from .microsoft_pb2 import * # noqa: F403
|
|
10
|
+
from .okta_pb2 import * # noqa: F403
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# NO CHECKED-IN PROTOBUF GENCODE
|
|
4
|
+
# source: viio/sync/v1/anthropic.proto
|
|
5
|
+
# Protobuf Python Version: 7.35.1
|
|
6
|
+
"""Generated protocol buffer code."""
|
|
7
|
+
from google.protobuf import descriptor as _descriptor
|
|
8
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
9
|
+
from google.protobuf import runtime_version as _runtime_version
|
|
10
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
11
|
+
from google.protobuf.internal import builder as _builder
|
|
12
|
+
_runtime_version.ValidateProtobufRuntimeVersion(
|
|
13
|
+
_runtime_version.Domain.PUBLIC,
|
|
14
|
+
7,
|
|
15
|
+
35,
|
|
16
|
+
1,
|
|
17
|
+
'',
|
|
18
|
+
'viio/sync/v1/anthropic.proto'
|
|
19
|
+
)
|
|
20
|
+
# @@protoc_insertion_point(imports)
|
|
21
|
+
|
|
22
|
+
_sym_db = _symbol_database.Default()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cviio/sync/v1/anthropic.proto\x12\x0cviio.sync.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xa4\x05\n\x12\x41nthropicChatUsage\x12/\n\x0bstarting_at\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12-\n\tending_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x17\n\naccount_id\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x17\n\napi_key_id\x18\x04 \x01(\tH\x01\x88\x01\x01\x12\x1f\n\x12service_account_id\x18\x05 \x01(\tH\x02\x88\x01\x01\x12\x19\n\x0cworkspace_id\x18\x06 \x01(\tH\x03\x88\x01\x01\x12\x12\n\x05model\x18\x07 \x01(\tH\x04\x88\x01\x01\x12\x19\n\x0cservice_tier\x18\x08 \x01(\tH\x05\x88\x01\x01\x12\x1b\n\x0e\x63ontext_window\x18\t \x01(\tH\x06\x88\x01\x01\x12\x1a\n\rinference_geo\x18\n \x01(\tH\x07\x88\x01\x01\x12\x1d\n\x15uncached_input_tokens\x18\x0b \x01(\x03\x12\x1f\n\x17\x63\x61\x63he_read_input_tokens\x18\x0c \x01(\x03\x12\x15\n\routput_tokens\x18\r \x01(\x03\x12<\n\x0e\x63\x61\x63he_creation\x18\x0e \x01(\x0b\x32$.viio.sync.v1.AnthropicCacheCreation\x12=\n\x0fserver_tool_use\x18\x0f \x01(\x0b\x32$.viio.sync.v1.AnthropicServerToolUseB\r\n\x0b_account_idB\r\n\x0b_api_key_idB\x15\n\x13_service_account_idB\x0f\n\r_workspace_idB\x08\n\x06_modelB\x0f\n\r_service_tierB\x11\n\x0f_context_windowB\x10\n\x0e_inference_geo\"^\n\x16\x41nthropicCacheCreation\x12!\n\x19\x65phemeral_1h_input_tokens\x18\x01 \x01(\x03\x12!\n\x19\x65phemeral_5m_input_tokens\x18\x02 \x01(\x03\"5\n\x16\x41nthropicServerToolUse\x12\x1b\n\x13web_search_requests\x18\x01 \x01(\x03\"\xdd\x03\n\rAnthropicCost\x12/\n\x0bstarting_at\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12-\n\tending_at\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06\x61mount\x18\x03 \x01(\t\x12\x10\n\x08\x63urrency\x18\x04 \x01(\t\x12\x19\n\x0cworkspace_id\x18\x05 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x06 \x01(\tH\x01\x88\x01\x01\x12\x16\n\tcost_type\x18\x07 \x01(\tH\x02\x88\x01\x01\x12\x17\n\ntoken_type\x18\x08 \x01(\tH\x03\x88\x01\x01\x12\x12\n\x05model\x18\t \x01(\tH\x04\x88\x01\x01\x12\x19\n\x0cservice_tier\x18\n \x01(\tH\x05\x88\x01\x01\x12\x1b\n\x0e\x63ontext_window\x18\x0b \x01(\tH\x06\x88\x01\x01\x12\x1a\n\rinference_geo\x18\x0c \x01(\tH\x07\x88\x01\x01\x42\x0f\n\r_workspace_idB\x0e\n\x0c_descriptionB\x0c\n\n_cost_typeB\r\n\x0b_token_typeB\x08\n\x06_modelB\x0f\n\r_service_tierB\x11\n\x0f_context_windowB\x10\n\x0e_inference_geo\"\x92\x04\n\x18\x41nthropicClaudeCodeUsage\x12(\n\x04\x64\x61te\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x35\n\x05\x61\x63tor\x18\x02 \x01(\x0b\x32&.viio.sync.v1.AnthropicClaudeCodeActor\x12\x1c\n\x0forganization_id\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x1a\n\rcustomer_type\x18\x04 \x01(\tH\x01\x88\x01\x01\x12\x1e\n\x11subscription_type\x18\x05 \x01(\tH\x02\x88\x01\x01\x12\x1a\n\rterminal_type\x18\x06 \x01(\tH\x03\x88\x01\x01\x12\x42\n\x0c\x63ore_metrics\x18\x07 \x01(\x0b\x32,.viio.sync.v1.AnthropicClaudeCodeCoreMetrics\x12\x42\n\x0ctool_actions\x18\x08 \x01(\x0b\x32,.viio.sync.v1.AnthropicClaudeCodeToolActions\x12I\n\x10model_breakdowns\x18\t \x03(\x0b\x32/.viio.sync.v1.AnthropicClaudeCodeModelBreakdownB\x12\n\x10_organization_idB\x10\n\x0e_customer_typeB\x14\n\x12_subscription_typeB\x10\n\x0e_terminal_type\"\x82\x01\n\x18\x41nthropicClaudeCodeActor\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x1a\n\remail_address\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x19\n\x0c\x61pi_key_name\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\x10\n\x0e_email_addressB\x0f\n\r_api_key_name\"\xc1\x01\n\x1e\x41nthropicClaudeCodeCoreMetrics\x12\x14\n\x0cnum_sessions\x18\x01 \x01(\x03\x12\x43\n\rlines_of_code\x18\x02 \x01(\x0b\x32,.viio.sync.v1.AnthropicClaudeCodeLinesOfCode\x12\x1e\n\x16\x63ommits_by_claude_code\x18\x03 \x01(\x03\x12$\n\x1cpull_requests_by_claude_code\x18\x04 \x01(\x03\"@\n\x1e\x41nthropicClaudeCodeLinesOfCode\x12\r\n\x05\x61\x64\x64\x65\x64\x18\x01 \x01(\x03\x12\x0f\n\x07removed\x18\x02 \x01(\x03\"\xb0\x02\n\x1e\x41nthropicClaudeCodeToolActions\x12>\n\tedit_tool\x18\x01 \x01(\x0b\x32+.viio.sync.v1.AnthropicClaudeCodeToolAction\x12\x44\n\x0fmulti_edit_tool\x18\x02 \x01(\x0b\x32+.viio.sync.v1.AnthropicClaudeCodeToolAction\x12?\n\nwrite_tool\x18\x03 \x01(\x0b\x32+.viio.sync.v1.AnthropicClaudeCodeToolAction\x12G\n\x12notebook_edit_tool\x18\x04 \x01(\x0b\x32+.viio.sync.v1.AnthropicClaudeCodeToolAction\"C\n\x1d\x41nthropicClaudeCodeToolAction\x12\x10\n\x08\x61\x63\x63\x65pted\x18\x01 \x01(\x03\x12\x10\n\x08rejected\x18\x02 \x01(\x03\"\xb3\x01\n!AnthropicClaudeCodeModelBreakdown\x12\r\n\x05model\x18\x01 \x01(\t\x12\x37\n\x06tokens\x18\x02 \x01(\x0b\x32\'.viio.sync.v1.AnthropicClaudeCodeTokens\x12\x46\n\x0e\x65stimated_cost\x18\x03 \x01(\x0b\x32..viio.sync.v1.AnthropicClaudeCodeEstimatedCost\"f\n\x19\x41nthropicClaudeCodeTokens\x12\r\n\x05input\x18\x01 \x01(\x03\x12\x0e\n\x06output\x18\x02 \x01(\x03\x12\x12\n\ncache_read\x18\x03 \x01(\x03\x12\x16\n\x0e\x63\x61\x63he_creation\x18\x04 \x01(\x03\"D\n AnthropicClaudeCodeEstimatedCost\x12\x10\n\x08\x63urrency\x18\x01 \x01(\t\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x03\x42\x1c\xaa\x02\x19Viio.SyncApi.V1.Contractsb\x06proto3')
|
|
29
|
+
|
|
30
|
+
_globals = globals()
|
|
31
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
32
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'viio.sync.v1.anthropic_pb2', _globals)
|
|
33
|
+
if not _descriptor._USE_C_DESCRIPTORS:
|
|
34
|
+
_globals['DESCRIPTOR']._loaded_options = None
|
|
35
|
+
_globals['DESCRIPTOR']._serialized_options = b'\252\002\031Viio.SyncApi.V1.Contracts'
|
|
36
|
+
_globals['_ANTHROPICCHATUSAGE']._serialized_start=80
|
|
37
|
+
_globals['_ANTHROPICCHATUSAGE']._serialized_end=756
|
|
38
|
+
_globals['_ANTHROPICCACHECREATION']._serialized_start=758
|
|
39
|
+
_globals['_ANTHROPICCACHECREATION']._serialized_end=852
|
|
40
|
+
_globals['_ANTHROPICSERVERTOOLUSE']._serialized_start=854
|
|
41
|
+
_globals['_ANTHROPICSERVERTOOLUSE']._serialized_end=907
|
|
42
|
+
_globals['_ANTHROPICCOST']._serialized_start=910
|
|
43
|
+
_globals['_ANTHROPICCOST']._serialized_end=1387
|
|
44
|
+
_globals['_ANTHROPICCLAUDECODEUSAGE']._serialized_start=1390
|
|
45
|
+
_globals['_ANTHROPICCLAUDECODEUSAGE']._serialized_end=1920
|
|
46
|
+
_globals['_ANTHROPICCLAUDECODEACTOR']._serialized_start=1923
|
|
47
|
+
_globals['_ANTHROPICCLAUDECODEACTOR']._serialized_end=2053
|
|
48
|
+
_globals['_ANTHROPICCLAUDECODECOREMETRICS']._serialized_start=2056
|
|
49
|
+
_globals['_ANTHROPICCLAUDECODECOREMETRICS']._serialized_end=2249
|
|
50
|
+
_globals['_ANTHROPICCLAUDECODELINESOFCODE']._serialized_start=2251
|
|
51
|
+
_globals['_ANTHROPICCLAUDECODELINESOFCODE']._serialized_end=2315
|
|
52
|
+
_globals['_ANTHROPICCLAUDECODETOOLACTIONS']._serialized_start=2318
|
|
53
|
+
_globals['_ANTHROPICCLAUDECODETOOLACTIONS']._serialized_end=2622
|
|
54
|
+
_globals['_ANTHROPICCLAUDECODETOOLACTION']._serialized_start=2624
|
|
55
|
+
_globals['_ANTHROPICCLAUDECODETOOLACTION']._serialized_end=2691
|
|
56
|
+
_globals['_ANTHROPICCLAUDECODEMODELBREAKDOWN']._serialized_start=2694
|
|
57
|
+
_globals['_ANTHROPICCLAUDECODEMODELBREAKDOWN']._serialized_end=2873
|
|
58
|
+
_globals['_ANTHROPICCLAUDECODETOKENS']._serialized_start=2875
|
|
59
|
+
_globals['_ANTHROPICCLAUDECODETOKENS']._serialized_end=2977
|
|
60
|
+
_globals['_ANTHROPICCLAUDECODEESTIMATEDCOST']._serialized_start=2979
|
|
61
|
+
_globals['_ANTHROPICCLAUDECODEESTIMATEDCOST']._serialized_end=3047
|
|
62
|
+
# @@protoc_insertion_point(module_scope)
|