Skip to content

Commit 8e5493f

Browse files
Add example CSV connector (#4)
1 parent 50a6207 commit 8e5493f

8 files changed

+808
-0
lines changed

csv/.isort.cfg

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[settings]
2+
profile = black

csv/README.md

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Elimity Insights example CSV connector
2+
3+
This Python package implements an example connector importing data from a CSV file.
4+
5+
## Usage
6+
7+
```console
8+
(venv) $ elimity-insights-example-csv-connector --help
9+
usage: elimity-insights-example-csv-connector [-h] --file FILE --source-id SOURCE_ID --source-token SOURCE_TOKEN --url URL
10+
11+
Example Elimity Insights custom connector importing from a CSV file
12+
13+
optional arguments:
14+
-h, --help show this help message and exit
15+
--file FILE path to the CSV file
16+
--source-id SOURCE_ID
17+
identifier for authenticating the source in Elimity Insights
18+
--source-token SOURCE_TOKEN
19+
token for authenticating the source in Elimity Insights
20+
--url URL URL of the Elimity Insights server
21+
```
22+
23+
## Example data
24+
25+
This example script expects the CSV file to contain users and roles, both with metadata.
26+
You can use the [`example.csv`](example.csv) file as a starting point.
27+
28+
## Data model for Elimity Insights
29+
30+
The data model for a custom source in Elimity Insights can be found in the [`data-model.json`](data-model.json) file.

csv/data-model.json

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"entityAttributeTypes": [
3+
{
4+
"archived": false,
5+
"description": "",
6+
"entityTypeId": "user",
7+
"id": "firstName",
8+
"name": "First name",
9+
"type": "string"
10+
},
11+
{
12+
"archived": false,
13+
"description": "",
14+
"entityTypeId": "user",
15+
"id": "lastName",
16+
"name": "Last name",
17+
"type": "string"
18+
},
19+
{
20+
"archived": false,
21+
"description": "",
22+
"entityTypeId": "user",
23+
"id": "lastLogon",
24+
"name": "Last logon",
25+
"type": "dateTime"
26+
},
27+
{
28+
"archived": false,
29+
"description": "",
30+
"entityTypeId": "role",
31+
"id": "securityLevel",
32+
"name": "Security level",
33+
"type": "number"
34+
}
35+
],
36+
"entityTypes": [
37+
{
38+
"anonymized": false,
39+
"icon": "person",
40+
"id": "user",
41+
"plural": "Users",
42+
"singular": "User"
43+
},
44+
{
45+
"anonymized": false,
46+
"icon": "label",
47+
"id": "role",
48+
"plural": "Roles",
49+
"singular": "Role"
50+
}
51+
],
52+
"relationshipAttributeTypes": [],
53+
"relationshipTypes": [
54+
{
55+
"fromEntityTypeId": "user",
56+
"toEntityTypeId": "role"
57+
}
58+
]
59+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
from argparse import ArgumentParser, Namespace
2+
from csv import DictReader
3+
from datetime import datetime
4+
from typing import (
5+
Annotated,
6+
Iterable,
7+
TypeVar,
8+
Callable,
9+
Literal,
10+
Union,
11+
)
12+
13+
from elimity_insights_client import (
14+
AttributeAssignment,
15+
Client,
16+
Config,
17+
DateTime,
18+
DateTimeValue,
19+
DomainGraph,
20+
Entity,
21+
NumberValue,
22+
Relationship,
23+
StringValue,
24+
Value,
25+
)
26+
from pydantic import BaseModel, parse_obj_as, BeforeValidator
27+
28+
29+
def main() -> None:
30+
args = _parser.parse_args()
31+
config = Config(args.source_id, args.url, args.source_token)
32+
client = Client(config)
33+
entities = _parse_rows(_parse_entity, args)
34+
relationships = _parse_rows(_parse_relationship, args)
35+
graph = DomainGraph(entities, relationships)
36+
client.reload_domain_graph(graph)
37+
38+
39+
def _validate_datetime(value: object) -> object:
40+
return datetime.min if value == "" else value
41+
42+
43+
_validator = BeforeValidator(_validate_datetime)
44+
_T = TypeVar("_T")
45+
46+
47+
class _Role(BaseModel):
48+
role_id: str
49+
role_name: str
50+
role_security_level: float
51+
type: Literal["role"]
52+
53+
54+
class _User(BaseModel):
55+
type: Literal["user"]
56+
user_display_name: str
57+
user_first_name: str
58+
user_id: str
59+
user_last_logon: Annotated[datetime, _validator]
60+
user_last_name: str
61+
62+
63+
class _UserRole(BaseModel):
64+
type: Literal["user_role"]
65+
user_role_rid: str
66+
user_role_uid: str
67+
68+
69+
_Row = Union[_Role, _User, _UserRole]
70+
71+
72+
def _make_attribute_assignments(
73+
assignments: dict[str, object]
74+
) -> Iterable[AttributeAssignment]:
75+
for attribute_type_id, value in assignments.items():
76+
for val in _make_value(value):
77+
yield AttributeAssignment(attribute_type_id, val)
78+
79+
80+
def _make_entity(
81+
assignments: dict[str, object], id: str, name: str, type: str
82+
) -> Entity:
83+
ass = _make_attribute_assignments(assignments)
84+
return Entity(ass, id, name, type)
85+
86+
87+
def _make_value(value: object) -> Iterable[Value]:
88+
if isinstance(value, datetime) and value != datetime.min:
89+
time = DateTime(
90+
value.year,
91+
value.month,
92+
value.day,
93+
value.hour,
94+
value.minute,
95+
value.second,
96+
)
97+
yield DateTimeValue(time)
98+
99+
if isinstance(value, float):
100+
yield NumberValue(value)
101+
102+
if isinstance(value, str):
103+
yield StringValue(value)
104+
105+
106+
def _parse_entity(row: _Row) -> Iterable[Entity]:
107+
if isinstance(row, _Role):
108+
role_assignments: dict[str, object] = {"securityLevel": row.role_security_level}
109+
yield _make_entity(role_assignments, row.role_id, row.role_name, "role")
110+
111+
if isinstance(row, _User):
112+
user_assignments: dict[str, object] = {
113+
"firstName": row.user_first_name,
114+
"lastLogon": row.user_last_logon,
115+
"lastName": row.user_last_name,
116+
}
117+
yield _make_entity(user_assignments, row.user_id, row.user_display_name, "user")
118+
119+
120+
def _parse_relationship(row: _Row) -> Iterable[Relationship]:
121+
if isinstance(row, _UserRole):
122+
assignments: list[AttributeAssignment] = []
123+
yield Relationship(
124+
assignments, row.user_role_uid, "user", row.user_role_rid, "role"
125+
)
126+
127+
128+
def _parse_rows(
129+
parse_function: Callable[[_Row], Iterable[_T]], namespace: Namespace
130+
) -> Iterable[_T]:
131+
with open(namespace.file, newline="") as file:
132+
reader = DictReader(file)
133+
for row in reader:
134+
r = parse_obj_as(_Row, row)
135+
yield from parse_function(r)
136+
137+
138+
def _add_flag(help: str, name: str, type_int: bool = False) -> None:
139+
_parser.add_argument(name, help=help, required=True, type=int if type_int else str)
140+
141+
142+
_parser = ArgumentParser(
143+
description="Example Elimity Insights custom connector importing from a CSV file"
144+
)
145+
_add_flag("path to the CSV file", "--file")
146+
_add_flag(
147+
"identifier for authenticating the source in Elimity Insights", "--source-id", True
148+
)
149+
_add_flag("token for authenticating the source in Elimity Insights", "--source-token")
150+
_add_flag("URL of the Elimity Insights server", "--url")

csv/example.csv

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
role_id,role_name,role_security_level,type,user_display_name,user_first_name,user_id,user_last_name,user_last_logon,user_role_rid,user_role_uid
2+
1,Reader,0,role,,,,,,,
3+
2,Writer,1,role,,,,,,,
4+
,,,user,dduck,Donald,1,Duck,1668603363,,
5+
,,,user,mmouse,Mickey,2,Mouse,,,
6+
,,,user_role,,,,,,1,1
7+
,,,user_role,,,,,,1,2
8+
,,,user_role,,,,,,2,2

csv/mypy.ini

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[mypy]
2+
disallow_any_explicit = True
3+
strict = True

0 commit comments

Comments
 (0)