viio-sync-api-sdk 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 (40) hide show
  1. viio_sync_api_sdk-0.1.0/.gitignore +6 -0
  2. viio_sync_api_sdk-0.1.0/CHANGELOG.md +10 -0
  3. viio_sync_api_sdk-0.1.0/LICENSE +21 -0
  4. viio_sync_api_sdk-0.1.0/PKG-INFO +145 -0
  5. viio_sync_api_sdk-0.1.0/README.md +123 -0
  6. viio_sync_api_sdk-0.1.0/examples/__init__.py +1 -0
  7. viio_sync_api_sdk-0.1.0/examples/aws_lambda.py +191 -0
  8. viio_sync_api_sdk-0.1.0/examples/requirements.txt +1 -0
  9. viio_sync_api_sdk-0.1.0/examples/template.yaml +59 -0
  10. viio_sync_api_sdk-0.1.0/pyproject.toml +74 -0
  11. viio_sync_api_sdk-0.1.0/src/viio_sync_api/__init__.py +27 -0
  12. viio_sync_api_sdk-0.1.0/src/viio_sync_api/_auth.py +88 -0
  13. viio_sync_api_sdk-0.1.0/src/viio_sync_api/_client.py +192 -0
  14. viio_sync_api_sdk-0.1.0/src/viio_sync_api/_endpoint.py +61 -0
  15. viio_sync_api_sdk-0.1.0/src/viio_sync_api/_errors.py +32 -0
  16. viio_sync_api_sdk-0.1.0/src/viio_sync_api/_options.py +35 -0
  17. viio_sync_api_sdk-0.1.0/src/viio_sync_api/_transport.py +154 -0
  18. viio_sync_api_sdk-0.1.0/src/viio_sync_api/py.typed +0 -0
  19. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/__init__.py +10 -0
  20. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/anthropic_pb2.py +62 -0
  21. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/anthropic_pb2.pyi +188 -0
  22. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/anthropic_pb2_grpc.py +24 -0
  23. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/bamboo_hr_pb2.py +38 -0
  24. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/bamboo_hr_pb2.pyi +47 -0
  25. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/bamboo_hr_pb2_grpc.py +24 -0
  26. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/contracts_pb2.py +160 -0
  27. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/contracts_pb2.pyi +510 -0
  28. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/contracts_pb2_grpc.py +226 -0
  29. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/generic_pb2.py +42 -0
  30. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/generic_pb2.pyi +63 -0
  31. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/generic_pb2_grpc.py +24 -0
  32. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/google_pb2.py +46 -0
  33. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/google_pb2.pyi +84 -0
  34. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/google_pb2_grpc.py +24 -0
  35. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/microsoft_pb2.py +40 -0
  36. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/microsoft_pb2.pyi +51 -0
  37. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/microsoft_pb2_grpc.py +24 -0
  38. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/okta_pb2.py +40 -0
  39. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/okta_pb2.pyi +47 -0
  40. viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/okta_pb2_grpc.py +24 -0
@@ -0,0 +1,6 @@
1
+ .mypy_cache/
2
+ .pytest_cache/
3
+ .ruff_cache/
4
+ .venv/
5
+ __pycache__/
6
+ dist/
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-07-31)
4
+
5
+
6
+ ### Features
7
+
8
+ * **sync-api:** add Python SDK ([#2313](https://github.com/viio-io/integration-domain/issues/2313)) ([dbbfcf5](https://github.com/viio-io/integration-domain/commit/dbbfcf585dc9d99f71fba6e1ef1d3c4e5f57f9f5))
9
+
10
+ ## Changelog
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Viio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: viio-sync-api-sdk
3
+ Version: 0.1.0
4
+ Summary: Async Python SDK for the Viio Sync API
5
+ Project-URL: Documentation, https://developers.viio.io/sync-api
6
+ Project-URL: Changelog, https://developers.viio.io/sync-api
7
+ Author: Viio
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: grpc,sdk,sync,viio
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Framework :: AsyncIO
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: grpcio<2,>=1.83
20
+ Requires-Dist: protobuf<8,>=7.35.1
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Viio Sync API SDK for Python
24
+
25
+ `viio-sync-api-sdk` contains the generated `viio.sync.v1` protobuf contract and an AsyncIO client for authentication and
26
+ explicit sync-batch lifecycle management.
27
+
28
+ ## Install
29
+
30
+ ```shell
31
+ python -m pip install viio-sync-api-sdk
32
+ ```
33
+
34
+ Python 3.11 and later are supported without an artificial upper minor-version cap. CI validates the minimum version and
35
+ the latest stable Python release, so the upper tested version advances automatically.
36
+
37
+ ## Send a batch
38
+
39
+ ```python
40
+ from viio_sync_api import SyncApiAuthentication, SyncApiClient, SyncApiClientOptions
41
+ from viio_sync_api.v1 import Account, AccountRecords, GenericAccountDetails, SyncRecordsRequest
42
+
43
+
44
+ async def sync_accounts() -> None:
45
+ options = SyncApiClientOptions(
46
+ endpoint="https://sync.example.com",
47
+ authentication=SyncApiAuthentication.api_key("your-api-key"),
48
+ )
49
+
50
+ async with SyncApiClient(options) as client:
51
+ batch = await client.start_batch("installation-id")
52
+ try:
53
+ await batch.write(
54
+ SyncRecordsRequest(
55
+ accounts=AccountRecords(
56
+ records=[
57
+ Account(
58
+ generic=GenericAccountDetails(
59
+ id="account-1",
60
+ email="person@example.com",
61
+ )
62
+ )
63
+ ]
64
+ )
65
+ )
66
+ )
67
+ await batch.complete()
68
+ except BaseException:
69
+ await batch.abort()
70
+ raise
71
+ ```
72
+
73
+ Each write accepts exactly one typed records wrapper. A present empty wrapper represents an intentional empty snapshot:
74
+
75
+ ```python
76
+ await batch.write(SyncRecordsRequest(accounts=AccountRecords()))
77
+ ```
78
+
79
+ `SyncApiBatchAlreadyInProgressError` distinguishes an existing sync. Protocol failures raise
80
+ `SyncApiProtocolError`. Native `grpc.aio.AioRpcError`, cancellation, and timeout errors remain available for retry
81
+ decisions.
82
+
83
+ The authenticated generated stub is available as `client.grpc_client`. Calls through it bypass batch validation.
84
+
85
+ `max_receive_message_size` caps individual responses received by this client, while `max_send_message_size` caps
86
+ individual requests it sends. The receive limit defaults to 16 MiB to match the .NET SDK; setting either value to
87
+ `None` leaves that direction at gRPC's default.
88
+
89
+ ## AWS Lambda
90
+
91
+ The [complete Lambda handler](https://github.com/viio-io/integration-domain/blob/main/sdk/python/examples/aws_lambda.py)
92
+ acquires an application token from Microsoft Entra ID, pages through Microsoft Graph users, maps each user to
93
+ `MicrosoftEmployeeDetails`, writes a complete employee snapshot, and completes or aborts the Sync API batch. The
94
+ [AWS SAM template](https://github.com/viio-io/integration-domain/blob/main/sdk/python/examples/template.yaml) configures
95
+ Amazon EventBridge Scheduler to invoke it every night with `cron(0 2 * * ? *)`.
96
+
97
+ Grant the Entra application the Microsoft Graph `User.Read.All` application permission and supply these Lambda
98
+ environment variables from your secret-management system:
99
+
100
+ - `MICROSOFT_TENANT_ID`
101
+ - `MICROSOFT_CLIENT_ID`
102
+ - `MICROSOFT_CLIENT_SECRET`
103
+ - `VIIO_SYNC_API_ENDPOINT`
104
+ - `VIIO_SYNC_API_KEY`
105
+ - `VIIO_DIRECT_INTEGRATION_INSTALLATION_ID`
106
+
107
+ The example creates and closes the client inside each invocation's event loop. This avoids retaining a gRPC AsyncIO
108
+ channel across event loops when Lambda reuses a warm execution environment.
109
+
110
+ `grpcio` contains native code. When creating a Lambda ZIP or layer on another operating system, ask pip for the
111
+ matching Linux wheel:
112
+
113
+ ```shell
114
+ # x86_64
115
+ python -m pip install \
116
+ --platform manylinux2014_x86_64 \
117
+ --implementation cp \
118
+ --python-version 3.11 \
119
+ --only-binary=:all: \
120
+ --target package \
121
+ viio-sync-api-sdk
122
+
123
+ # arm64
124
+ python -m pip install \
125
+ --platform manylinux2014_aarch64 \
126
+ --implementation cp \
127
+ --python-version 3.11 \
128
+ --only-binary=:all: \
129
+ --target package \
130
+ viio-sync-api-sdk
131
+ ```
132
+
133
+ Replace `3.11` with the Python version of the target Lambda runtime. The same package can be installed normally in AWS
134
+ Lambda Python container images. Full API documentation is available at <https://developers.viio.io/sync-api>.
135
+
136
+ ## Authentication and transport security
137
+
138
+ External integrations use API keys. Viio-managed callers can supply an asynchronous bearer-token provider.
139
+
140
+ Sync API endpoints must use HTTPS. HTTP is accepted only for loopback endpoints during development. SDK exception
141
+ messages and representations do not include keys or bearer tokens.
142
+
143
+ ## License
144
+
145
+ MIT
@@ -0,0 +1,123 @@
1
+ # Viio Sync API SDK for Python
2
+
3
+ `viio-sync-api-sdk` contains the generated `viio.sync.v1` protobuf contract and an AsyncIO client for authentication and
4
+ explicit sync-batch lifecycle management.
5
+
6
+ ## Install
7
+
8
+ ```shell
9
+ python -m pip install viio-sync-api-sdk
10
+ ```
11
+
12
+ Python 3.11 and later are supported without an artificial upper minor-version cap. CI validates the minimum version and
13
+ the latest stable Python release, so the upper tested version advances automatically.
14
+
15
+ ## Send a batch
16
+
17
+ ```python
18
+ from viio_sync_api import SyncApiAuthentication, SyncApiClient, SyncApiClientOptions
19
+ from viio_sync_api.v1 import Account, AccountRecords, GenericAccountDetails, SyncRecordsRequest
20
+
21
+
22
+ async def sync_accounts() -> None:
23
+ options = SyncApiClientOptions(
24
+ endpoint="https://sync.example.com",
25
+ authentication=SyncApiAuthentication.api_key("your-api-key"),
26
+ )
27
+
28
+ async with SyncApiClient(options) as client:
29
+ batch = await client.start_batch("installation-id")
30
+ try:
31
+ await batch.write(
32
+ SyncRecordsRequest(
33
+ accounts=AccountRecords(
34
+ records=[
35
+ Account(
36
+ generic=GenericAccountDetails(
37
+ id="account-1",
38
+ email="person@example.com",
39
+ )
40
+ )
41
+ ]
42
+ )
43
+ )
44
+ )
45
+ await batch.complete()
46
+ except BaseException:
47
+ await batch.abort()
48
+ raise
49
+ ```
50
+
51
+ Each write accepts exactly one typed records wrapper. A present empty wrapper represents an intentional empty snapshot:
52
+
53
+ ```python
54
+ await batch.write(SyncRecordsRequest(accounts=AccountRecords()))
55
+ ```
56
+
57
+ `SyncApiBatchAlreadyInProgressError` distinguishes an existing sync. Protocol failures raise
58
+ `SyncApiProtocolError`. Native `grpc.aio.AioRpcError`, cancellation, and timeout errors remain available for retry
59
+ decisions.
60
+
61
+ The authenticated generated stub is available as `client.grpc_client`. Calls through it bypass batch validation.
62
+
63
+ `max_receive_message_size` caps individual responses received by this client, while `max_send_message_size` caps
64
+ individual requests it sends. The receive limit defaults to 16 MiB to match the .NET SDK; setting either value to
65
+ `None` leaves that direction at gRPC's default.
66
+
67
+ ## AWS Lambda
68
+
69
+ The [complete Lambda handler](https://github.com/viio-io/integration-domain/blob/main/sdk/python/examples/aws_lambda.py)
70
+ acquires an application token from Microsoft Entra ID, pages through Microsoft Graph users, maps each user to
71
+ `MicrosoftEmployeeDetails`, writes a complete employee snapshot, and completes or aborts the Sync API batch. The
72
+ [AWS SAM template](https://github.com/viio-io/integration-domain/blob/main/sdk/python/examples/template.yaml) configures
73
+ Amazon EventBridge Scheduler to invoke it every night with `cron(0 2 * * ? *)`.
74
+
75
+ Grant the Entra application the Microsoft Graph `User.Read.All` application permission and supply these Lambda
76
+ environment variables from your secret-management system:
77
+
78
+ - `MICROSOFT_TENANT_ID`
79
+ - `MICROSOFT_CLIENT_ID`
80
+ - `MICROSOFT_CLIENT_SECRET`
81
+ - `VIIO_SYNC_API_ENDPOINT`
82
+ - `VIIO_SYNC_API_KEY`
83
+ - `VIIO_DIRECT_INTEGRATION_INSTALLATION_ID`
84
+
85
+ The example creates and closes the client inside each invocation's event loop. This avoids retaining a gRPC AsyncIO
86
+ channel across event loops when Lambda reuses a warm execution environment.
87
+
88
+ `grpcio` contains native code. When creating a Lambda ZIP or layer on another operating system, ask pip for the
89
+ matching Linux wheel:
90
+
91
+ ```shell
92
+ # x86_64
93
+ python -m pip install \
94
+ --platform manylinux2014_x86_64 \
95
+ --implementation cp \
96
+ --python-version 3.11 \
97
+ --only-binary=:all: \
98
+ --target package \
99
+ viio-sync-api-sdk
100
+
101
+ # arm64
102
+ python -m pip install \
103
+ --platform manylinux2014_aarch64 \
104
+ --implementation cp \
105
+ --python-version 3.11 \
106
+ --only-binary=:all: \
107
+ --target package \
108
+ viio-sync-api-sdk
109
+ ```
110
+
111
+ Replace `3.11` with the Python version of the target Lambda runtime. The same package can be installed normally in AWS
112
+ Lambda Python container images. Full API documentation is available at <https://developers.viio.io/sync-api>.
113
+
114
+ ## Authentication and transport security
115
+
116
+ External integrations use API keys. Viio-managed callers can supply an asynchronous bearer-token provider.
117
+
118
+ Sync API endpoints must use HTTPS. HTTP is accepted only for loopback endpoints during development. SDK exception
119
+ messages and representations do not include keys or bearer tokens.
120
+
121
+ ## License
122
+
123
+ MIT
@@ -0,0 +1 @@
1
+ """Runnable SDK examples."""
@@ -0,0 +1,191 @@
1
+ """Nightly Microsoft Graph employee sync triggered by an EventBridge cron schedule.
2
+
3
+ Grant the Microsoft Entra application the ``User.Read.All`` application permission and
4
+ configure the environment variables documented in the package readme.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import os
10
+ from typing import Any
11
+ from urllib.parse import urlencode, urlsplit
12
+ from urllib.request import Request, urlopen
13
+
14
+ from viio_sync_api import SyncApiAuthentication, SyncApiClient, SyncApiClientOptions
15
+ from viio_sync_api.v1 import (
16
+ Employee,
17
+ EmployeeRecords,
18
+ MicrosoftEmployeeDetails,
19
+ MicrosoftEmployeeOrgData,
20
+ SyncRecordsRequest,
21
+ )
22
+
23
+ _GRAPH_USERS_URL = "https://graph.microsoft.com/v1.0/users?" + urlencode(
24
+ {
25
+ "$select": ",".join(
26
+ (
27
+ "id",
28
+ "displayName",
29
+ "givenName",
30
+ "surname",
31
+ "mail",
32
+ "userPrincipalName",
33
+ "department",
34
+ "accountEnabled",
35
+ "createdDateTime",
36
+ "deletedDateTime",
37
+ "jobTitle",
38
+ "userType",
39
+ "country",
40
+ "employeeOrgData",
41
+ )
42
+ ),
43
+ "$top": "999",
44
+ }
45
+ )
46
+
47
+
48
+ def _request_graph_token() -> str:
49
+ tenant_id = os.environ["MICROSOFT_TENANT_ID"]
50
+ if not tenant_id or any(
51
+ character not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-" for character in tenant_id
52
+ ):
53
+ raise ValueError("MICROSOFT_TENANT_ID must be a tenant ID or verified domain name")
54
+
55
+ request = Request(
56
+ f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token",
57
+ data=urlencode(
58
+ {
59
+ "grant_type": "client_credentials",
60
+ "client_id": os.environ["MICROSOFT_CLIENT_ID"],
61
+ "client_secret": os.environ["MICROSOFT_CLIENT_SECRET"],
62
+ "scope": "https://graph.microsoft.com/.default",
63
+ }
64
+ ).encode(),
65
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
66
+ method="POST",
67
+ )
68
+ payload = _request_json(request)
69
+ token = payload.get("access_token")
70
+ if not isinstance(token, str) or not token:
71
+ raise RuntimeError("Microsoft identity platform returned no access token")
72
+ return token
73
+
74
+
75
+ def _request_graph_users(url: str, access_token: str) -> tuple[list[dict[str, Any]], str | None]:
76
+ parsed_url = urlsplit(url)
77
+ if parsed_url.scheme != "https" or parsed_url.hostname != "graph.microsoft.com":
78
+ raise RuntimeError("Microsoft Graph returned an invalid pagination URL")
79
+
80
+ payload = _request_json(
81
+ Request(
82
+ url,
83
+ headers={
84
+ "Accept": "application/json",
85
+ "Authorization": f"Bearer {access_token}",
86
+ },
87
+ )
88
+ )
89
+ users = payload.get("value")
90
+ if not isinstance(users, list) or not all(isinstance(user, dict) for user in users):
91
+ raise RuntimeError("Microsoft Graph returned an invalid users response")
92
+
93
+ next_url = payload.get("@odata.nextLink")
94
+ if next_url is not None and not isinstance(next_url, str):
95
+ raise RuntimeError("Microsoft Graph returned an invalid pagination URL")
96
+ return users, next_url
97
+
98
+
99
+ def _request_json(request: Request) -> dict[str, Any]:
100
+ with urlopen(request, timeout=30) as response:
101
+ payload = json.load(response)
102
+ if not isinstance(payload, dict):
103
+ raise RuntimeError("Remote service returned an invalid JSON response")
104
+ return payload
105
+
106
+
107
+ def _employee_from_graph_user(user: dict[str, Any]) -> Employee:
108
+ details = MicrosoftEmployeeDetails(
109
+ id=_required_string(user, "id"),
110
+ display_name=_required_string(user, "displayName"),
111
+ user_principal_name=_required_string(user, "userPrincipalName"),
112
+ )
113
+ for graph_name, proto_name in (
114
+ ("givenName", "given_name"),
115
+ ("surname", "surname"),
116
+ ("mail", "mail"),
117
+ ("department", "department"),
118
+ ("jobTitle", "job_title"),
119
+ ("userType", "user_type"),
120
+ ("country", "country"),
121
+ ):
122
+ value = user.get(graph_name)
123
+ if isinstance(value, str) and value:
124
+ setattr(details, proto_name, value)
125
+
126
+ account_enabled = user.get("accountEnabled")
127
+ if isinstance(account_enabled, bool):
128
+ details.account_enabled = account_enabled
129
+
130
+ created_date_time = user.get("createdDateTime")
131
+ if isinstance(created_date_time, str) and created_date_time:
132
+ details.created_date_time.FromJsonString(created_date_time)
133
+
134
+ deleted_date_time = user.get("deletedDateTime")
135
+ if isinstance(deleted_date_time, str) and deleted_date_time:
136
+ details.deleted_date_time.FromJsonString(deleted_date_time)
137
+
138
+ org_data = user.get("employeeOrgData")
139
+ if isinstance(org_data, dict):
140
+ mapped_org_data = MicrosoftEmployeeOrgData()
141
+ division = org_data.get("division")
142
+ if isinstance(division, str) and division:
143
+ mapped_org_data.division = division
144
+ cost_center = org_data.get("costCenter")
145
+ if isinstance(cost_center, str) and cost_center:
146
+ mapped_org_data.cost_center = cost_center
147
+ if mapped_org_data.ListFields():
148
+ details.employee_org_data.CopyFrom(mapped_org_data)
149
+
150
+ return Employee(microsoft=details)
151
+
152
+
153
+ def _required_string(source: dict[str, Any], field: str) -> str:
154
+ value = source.get(field)
155
+ if not isinstance(value, str) or not value:
156
+ raise RuntimeError(f"Microsoft Graph user is missing {field}")
157
+ return value
158
+
159
+
160
+ async def _sync_employees() -> int:
161
+ graph_token = await asyncio.to_thread(_request_graph_token)
162
+ options = SyncApiClientOptions(
163
+ endpoint=os.environ["VIIO_SYNC_API_ENDPOINT"],
164
+ authentication=SyncApiAuthentication.api_key(os.environ["VIIO_SYNC_API_KEY"]),
165
+ )
166
+ async with SyncApiClient(options) as client:
167
+ batch = await client.start_batch(os.environ["VIIO_DIRECT_INTEGRATION_INSTALLATION_ID"])
168
+ employee_count = 0
169
+ next_url: str | None = _GRAPH_USERS_URL
170
+ try:
171
+ while next_url is not None:
172
+ users, next_url = await asyncio.to_thread(_request_graph_users, next_url, graph_token)
173
+ if users:
174
+ employees = [_employee_from_graph_user(user) for user in users]
175
+ await batch.write(SyncRecordsRequest(employees=EmployeeRecords(records=employees)))
176
+ employee_count += len(employees)
177
+
178
+ if employee_count == 0:
179
+ await batch.write(SyncRecordsRequest(employees=EmployeeRecords()))
180
+ await batch.complete()
181
+ except BaseException:
182
+ await batch.abort()
183
+ raise
184
+ return employee_count
185
+
186
+
187
+ def lambda_handler(event: dict[str, Any], context: object) -> dict[str, Any]:
188
+ """Run one complete employee snapshot for the scheduled invocation."""
189
+ del event, context
190
+ employee_count = asyncio.run(_sync_employees())
191
+ return {"ok": True, "employee_count": employee_count}
@@ -0,0 +1 @@
1
+ viio-sync-api-sdk
@@ -0,0 +1,59 @@
1
+ ---
2
+ AWSTemplateFormatVersion: "2010-09-09"
3
+ Transform: AWS::Serverless-2016-10-31
4
+ Description: Nightly Microsoft Graph employee sync to the Viio Sync API
5
+
6
+ Parameters:
7
+ MicrosoftTenantId:
8
+ Type: String
9
+ MicrosoftClientId:
10
+ Type: String
11
+ MicrosoftClientSecret:
12
+ Type: String
13
+ NoEcho: true
14
+ ViioSyncApiEndpoint:
15
+ Type: String
16
+ ViioSyncApiKey:
17
+ Type: String
18
+ NoEcho: true
19
+ ViioDirectIntegrationInstallationId:
20
+ Type: String
21
+
22
+ Resources:
23
+ EmployeeSyncFunction:
24
+ Type: AWS::Serverless::Function
25
+ Metadata:
26
+ checkov:
27
+ skip:
28
+ - id: CKV_AWS_115
29
+ comment: Reserved concurrency is deployment-specific and omitted from this example.
30
+ - id: CKV_AWS_116
31
+ comment: A production deployment should configure its preferred failure destination.
32
+ - id: CKV_AWS_117
33
+ comment: This example calls public Microsoft Graph and Viio API endpoints directly.
34
+ - id: CKV_AWS_173
35
+ comment: A production deployment should supply its preferred customer-managed KMS key.
36
+ Properties:
37
+ Architectures:
38
+ - arm64
39
+ CodeUri: .
40
+ Handler: aws_lambda.lambda_handler
41
+ Runtime: python3.11
42
+ MemorySize: 512
43
+ Timeout: 900
44
+ Environment:
45
+ Variables:
46
+ MICROSOFT_TENANT_ID: !Ref MicrosoftTenantId
47
+ MICROSOFT_CLIENT_ID: !Ref MicrosoftClientId
48
+ MICROSOFT_CLIENT_SECRET: !Ref MicrosoftClientSecret
49
+ VIIO_SYNC_API_ENDPOINT: !Ref ViioSyncApiEndpoint
50
+ VIIO_SYNC_API_KEY: !Ref ViioSyncApiKey
51
+ VIIO_DIRECT_INTEGRATION_INSTALLATION_ID:
52
+ Ref: ViioDirectIntegrationInstallationId
53
+ Events:
54
+ NightlyEmployeeSync:
55
+ Type: ScheduleV2
56
+ Properties:
57
+ Description: Synchronize Microsoft Graph employees every night
58
+ ScheduleExpression: cron(0 2 * * ? *)
59
+ ScheduleExpressionTimezone: UTC
@@ -0,0 +1,74 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27,<2"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "viio-sync-api-sdk"
7
+ version = "0.1.0"
8
+ description = "Async Python SDK for the Viio Sync API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ authors = [{ name = "Viio" }]
13
+ keywords = ["viio", "sync", "grpc", "sdk"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Framework :: AsyncIO",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Typing :: Typed",
22
+ ]
23
+ dependencies = [
24
+ "grpcio>=1.83,<2",
25
+ "protobuf>=7.35.1,<8",
26
+ ]
27
+
28
+ [project.urls]
29
+ Documentation = "https://developers.viio.io/sync-api"
30
+ Changelog = "https://developers.viio.io/sync-api"
31
+
32
+ [dependency-groups]
33
+ dev = [
34
+ "build>=1.3,<2",
35
+ "grpcio-tools>=1.76,<2",
36
+ "hatchling>=1.27,<2",
37
+ "mypy>=1.17,<2",
38
+ "pytest>=8.4,<10",
39
+ "pytest-asyncio>=1.1,<2",
40
+ "ruff>=0.12,<1",
41
+ "types-grpcio>=1.0,<2",
42
+ ]
43
+
44
+ [tool.hatch.build.targets.sdist]
45
+ include = [
46
+ "/src",
47
+ "/examples",
48
+ "/README.md",
49
+ "/CHANGELOG.md",
50
+ "/LICENSE",
51
+ ]
52
+
53
+ [tool.hatch.build.targets.wheel]
54
+ packages = ["src/viio_sync_api"]
55
+
56
+ [tool.pytest.ini_options]
57
+ addopts = "-ra"
58
+ asyncio_mode = "auto"
59
+ pythonpath = ["."]
60
+ testpaths = ["tests"]
61
+
62
+ [tool.ruff]
63
+ extend-exclude = ["src/viio_sync_api/v1/*_pb2*"]
64
+ line-length = 120
65
+ target-version = "py311"
66
+
67
+ [tool.ruff.lint]
68
+ select = ["E", "F", "I", "UP", "B", "ASYNC", "RUF"]
69
+
70
+ [tool.mypy]
71
+ exclude = ["src/viio_sync_api/v1/.*_pb2.*\\.py"]
72
+ python_version = "3.11"
73
+ strict = true
74
+ packages = ["viio_sync_api"]
@@ -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
+ ]