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.
- viio_sync_api_sdk-0.1.0/.gitignore +6 -0
- viio_sync_api_sdk-0.1.0/CHANGELOG.md +10 -0
- viio_sync_api_sdk-0.1.0/LICENSE +21 -0
- viio_sync_api_sdk-0.1.0/PKG-INFO +145 -0
- viio_sync_api_sdk-0.1.0/README.md +123 -0
- viio_sync_api_sdk-0.1.0/examples/__init__.py +1 -0
- viio_sync_api_sdk-0.1.0/examples/aws_lambda.py +191 -0
- viio_sync_api_sdk-0.1.0/examples/requirements.txt +1 -0
- viio_sync_api_sdk-0.1.0/examples/template.yaml +59 -0
- viio_sync_api_sdk-0.1.0/pyproject.toml +74 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/__init__.py +27 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/_auth.py +88 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/_client.py +192 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/_endpoint.py +61 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/_errors.py +32 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/_options.py +35 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/_transport.py +154 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/py.typed +0 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/__init__.py +10 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/anthropic_pb2.py +62 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/anthropic_pb2.pyi +188 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/anthropic_pb2_grpc.py +24 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/bamboo_hr_pb2.py +38 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/bamboo_hr_pb2.pyi +47 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/bamboo_hr_pb2_grpc.py +24 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/contracts_pb2.py +160 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/contracts_pb2.pyi +510 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/contracts_pb2_grpc.py +226 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/generic_pb2.py +42 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/generic_pb2.pyi +63 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/generic_pb2_grpc.py +24 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/google_pb2.py +46 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/google_pb2.pyi +84 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/google_pb2_grpc.py +24 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/microsoft_pb2.py +40 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/microsoft_pb2.pyi +51 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/microsoft_pb2_grpc.py +24 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/okta_pb2.py +40 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/okta_pb2.pyi +47 -0
- viio_sync_api_sdk-0.1.0/src/viio_sync_api/v1/okta_pb2_grpc.py +24 -0
|
@@ -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
|
+
]
|