fluidattacks_refiner_sdk 0.1.1__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.
- fluidattacks_refiner_sdk/__init__.py +1 -0
- fluidattacks_refiner_sdk/auth/__init__.py +7 -0
- fluidattacks_refiner_sdk/auth/_core.py +9 -0
- fluidattacks_refiner_sdk/client/__init__.py +33 -0
- fluidattacks_refiner_sdk/client/_common.py +123 -0
- fluidattacks_refiner_sdk/client/_get_contacts.py +212 -0
- fluidattacks_refiner_sdk/client/_get_forms.py +144 -0
- fluidattacks_refiner_sdk/client/_get_reporting.py +140 -0
- fluidattacks_refiner_sdk/client/_get_responses.py +289 -0
- fluidattacks_refiner_sdk/core.py +124 -0
- fluidattacks_refiner_sdk/py.typed +0 -0
- fluidattacks_refiner_sdk-0.1.1.dist-info/METADATA +11 -0
- fluidattacks_refiner_sdk-0.1.1.dist-info/RECORD +14 -0
- fluidattacks_refiner_sdk-0.1.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.1"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from logging import (
|
|
2
|
+
Logger,
|
|
3
|
+
)
|
|
4
|
+
|
|
5
|
+
from fluidattacks_refiner_sdk.auth import (
|
|
6
|
+
Credentials,
|
|
7
|
+
)
|
|
8
|
+
from fluidattacks_refiner_sdk.client._get_contacts import (
|
|
9
|
+
get_contacts,
|
|
10
|
+
)
|
|
11
|
+
from fluidattacks_refiner_sdk.client._get_forms import (
|
|
12
|
+
get_forms,
|
|
13
|
+
)
|
|
14
|
+
from fluidattacks_refiner_sdk.client._get_reporting import (
|
|
15
|
+
get_nps_metrics,
|
|
16
|
+
)
|
|
17
|
+
from fluidattacks_refiner_sdk.client._get_responses import (
|
|
18
|
+
get_responses,
|
|
19
|
+
)
|
|
20
|
+
from fluidattacks_refiner_sdk.core import (
|
|
21
|
+
ApiClient,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = ["new_api_client"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def new_api_client(log: Logger, creds: Credentials) -> ApiClient:
|
|
28
|
+
return ApiClient(
|
|
29
|
+
get_forms=get_forms(log, creds),
|
|
30
|
+
get_responses=lambda form_id: get_responses(log, creds, form_id),
|
|
31
|
+
get_contacts=get_contacts(log, creds),
|
|
32
|
+
get_nps_metrics=get_nps_metrics(log, creds),
|
|
33
|
+
)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import logging
|
|
3
|
+
from collections.abc import (
|
|
4
|
+
Callable,
|
|
5
|
+
)
|
|
6
|
+
from dataclasses import (
|
|
7
|
+
dataclass,
|
|
8
|
+
)
|
|
9
|
+
from typing import (
|
|
10
|
+
Generic,
|
|
11
|
+
TypeVar,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from fa_purity import (
|
|
15
|
+
Cmd,
|
|
16
|
+
FrozenList,
|
|
17
|
+
Maybe,
|
|
18
|
+
Result,
|
|
19
|
+
ResultE,
|
|
20
|
+
cast_exception,
|
|
21
|
+
)
|
|
22
|
+
from fa_purity.json import (
|
|
23
|
+
JsonObj,
|
|
24
|
+
JsonPrimitiveUnfolder,
|
|
25
|
+
JsonUnfolder,
|
|
26
|
+
Unfolder,
|
|
27
|
+
)
|
|
28
|
+
from fluidattacks_etl_utils.bug import (
|
|
29
|
+
Bug,
|
|
30
|
+
)
|
|
31
|
+
from pure_requests.response import handle_status
|
|
32
|
+
from pure_requests.retry import HandledErrorFactory, cmd_if_fail, retry_cmd, sleep_cmd
|
|
33
|
+
from requests import HTTPError, RequestException, Response
|
|
34
|
+
|
|
35
|
+
_T = TypeVar("_T")
|
|
36
|
+
LOG = logging.getLogger(__name__)
|
|
37
|
+
_HTTP_5XX_START = 500
|
|
38
|
+
_HTTP_5XX_END = 600
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class DataPage(Generic[_T]):
|
|
43
|
+
items: FrozenList[_T]
|
|
44
|
+
next_cursor: Maybe[str]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def decode_items(
|
|
48
|
+
raw: JsonObj,
|
|
49
|
+
decode: Callable[[JsonObj], ResultE[_T]],
|
|
50
|
+
) -> ResultE[FrozenList[_T]]:
|
|
51
|
+
return JsonUnfolder.require(
|
|
52
|
+
raw,
|
|
53
|
+
"items",
|
|
54
|
+
lambda v: Unfolder.to_list_of(v, lambda j: Unfolder.to_json(j).bind(decode)),
|
|
55
|
+
).alt(
|
|
56
|
+
lambda e: Bug[Exception].new(
|
|
57
|
+
"decode_items",
|
|
58
|
+
inspect.currentframe(),
|
|
59
|
+
e,
|
|
60
|
+
(JsonUnfolder.dumps(raw),),
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def decode_next_cursor(raw: JsonObj) -> ResultE[Maybe[str]]:
|
|
66
|
+
return JsonUnfolder.require(
|
|
67
|
+
raw,
|
|
68
|
+
"pagination",
|
|
69
|
+
lambda v: Unfolder.to_json(v).bind(
|
|
70
|
+
lambda pag: JsonUnfolder.optional(
|
|
71
|
+
pag,
|
|
72
|
+
"next_page_cursor",
|
|
73
|
+
lambda vv: Unfolder.to_primitive(vv).bind(JsonPrimitiveUnfolder.to_str),
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def decode_page(
|
|
80
|
+
raw: JsonObj,
|
|
81
|
+
decode: Callable[[JsonObj], ResultE[_T]],
|
|
82
|
+
) -> ResultE[DataPage[_T]]:
|
|
83
|
+
items = decode_items(raw, decode)
|
|
84
|
+
cursor = decode_next_cursor(raw)
|
|
85
|
+
return items.bind(lambda i: cursor.map(lambda c: DataPage(i, c))).alt(
|
|
86
|
+
lambda e: Bug[Exception].new(
|
|
87
|
+
"decode_page",
|
|
88
|
+
inspect.currentframe(),
|
|
89
|
+
e,
|
|
90
|
+
(JsonUnfolder.dumps(raw),),
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def with_retry_handler(
|
|
96
|
+
raw_page: Cmd[Result[Response, RequestException]],
|
|
97
|
+
retries: int,
|
|
98
|
+
) -> Cmd[ResultE[Response]]:
|
|
99
|
+
error_factory: HandledErrorFactory[HTTPError, Exception] = HandledErrorFactory()
|
|
100
|
+
handled_get_page = raw_page.map(
|
|
101
|
+
lambda r: (
|
|
102
|
+
r.alt(cast_exception)
|
|
103
|
+
.alt(error_factory.unhandled)
|
|
104
|
+
.bind(
|
|
105
|
+
lambda r: handle_status(r).alt(
|
|
106
|
+
lambda e: (
|
|
107
|
+
error_factory.handled(e)
|
|
108
|
+
if e.errno and e.errno >= _HTTP_5XX_START and e.errno < _HTTP_5XX_END
|
|
109
|
+
else error_factory.unhandled(cast_exception(e))
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
return retry_cmd(
|
|
116
|
+
handled_get_page,
|
|
117
|
+
lambda i, r: cmd_if_fail(
|
|
118
|
+
r,
|
|
119
|
+
Cmd[None].wrap_impure(lambda: LOG.info("Error handled i.e. %s", r))
|
|
120
|
+
+ sleep_cmd(10 + 60 * (i - 1)),
|
|
121
|
+
),
|
|
122
|
+
retries,
|
|
123
|
+
)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import logging
|
|
3
|
+
from logging import (
|
|
4
|
+
Logger,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
from dateutil.parser import parse as parse_dt
|
|
8
|
+
from fa_purity import (
|
|
9
|
+
Cmd,
|
|
10
|
+
FrozenDict,
|
|
11
|
+
FrozenList,
|
|
12
|
+
Maybe,
|
|
13
|
+
Result,
|
|
14
|
+
ResultE,
|
|
15
|
+
ResultSmash,
|
|
16
|
+
Stream,
|
|
17
|
+
cast_exception,
|
|
18
|
+
)
|
|
19
|
+
from fa_purity.json import (
|
|
20
|
+
JsonObj,
|
|
21
|
+
JsonPrimitive,
|
|
22
|
+
JsonPrimitiveUnfolder,
|
|
23
|
+
JsonUnfolder,
|
|
24
|
+
JsonValue,
|
|
25
|
+
UnfoldedFactory,
|
|
26
|
+
Unfolder,
|
|
27
|
+
)
|
|
28
|
+
from fluidattacks_etl_utils.bug import (
|
|
29
|
+
Bug,
|
|
30
|
+
)
|
|
31
|
+
from fluidattacks_etl_utils.paginate import (
|
|
32
|
+
cursor_pagination,
|
|
33
|
+
)
|
|
34
|
+
from pure_requests.basic import (
|
|
35
|
+
HttpClient,
|
|
36
|
+
HttpClientFactory,
|
|
37
|
+
Params,
|
|
38
|
+
)
|
|
39
|
+
from pure_requests.response import (
|
|
40
|
+
json_decode,
|
|
41
|
+
)
|
|
42
|
+
from requests import (
|
|
43
|
+
Response,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
from fluidattacks_refiner_sdk.auth import (
|
|
47
|
+
Credentials,
|
|
48
|
+
)
|
|
49
|
+
from fluidattacks_refiner_sdk.client._common import (
|
|
50
|
+
DataPage,
|
|
51
|
+
decode_items,
|
|
52
|
+
decode_next_cursor,
|
|
53
|
+
with_retry_handler,
|
|
54
|
+
)
|
|
55
|
+
from fluidattacks_refiner_sdk.core import (
|
|
56
|
+
Account,
|
|
57
|
+
AccountDomain,
|
|
58
|
+
AccountId,
|
|
59
|
+
ContactId,
|
|
60
|
+
Person,
|
|
61
|
+
relative_endpoint,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
LOG = logging.getLogger(__name__)
|
|
65
|
+
|
|
66
|
+
_MAX_PAGE_LENGTH = 1000
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _iso_to_unix(iso: str) -> ResultE[int]:
|
|
70
|
+
try:
|
|
71
|
+
return Result[int, Exception].success(int(parse_dt(iso).timestamp()))
|
|
72
|
+
except (ValueError, OverflowError) as e:
|
|
73
|
+
return Result[int, Exception].failure(e)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _decode_account_obj(obj: JsonObj) -> ResultE[Account]:
|
|
77
|
+
acc_id = JsonUnfolder.require(
|
|
78
|
+
obj,
|
|
79
|
+
"uuid",
|
|
80
|
+
lambda vv: Unfolder.to_primitive(vv).bind(JsonPrimitiveUnfolder.to_str),
|
|
81
|
+
).map(AccountId)
|
|
82
|
+
remote_id = JsonUnfolder.optional(
|
|
83
|
+
obj,
|
|
84
|
+
"remote_id",
|
|
85
|
+
lambda vv: Unfolder.to_primitive(vv).bind(JsonPrimitiveUnfolder.to_str),
|
|
86
|
+
)
|
|
87
|
+
display_name = JsonUnfolder.optional(
|
|
88
|
+
obj,
|
|
89
|
+
"display_name",
|
|
90
|
+
lambda vv: Unfolder.to_primitive(vv).bind(JsonPrimitiveUnfolder.to_str),
|
|
91
|
+
)
|
|
92
|
+
domain = JsonUnfolder.optional(
|
|
93
|
+
obj,
|
|
94
|
+
"domain",
|
|
95
|
+
lambda vv: Unfolder.to_primitive(vv).bind(JsonPrimitiveUnfolder.to_str),
|
|
96
|
+
).map(lambda m: m.map(AccountDomain))
|
|
97
|
+
return ResultSmash.smash_result_4(acc_id, remote_id, display_name, domain).map(
|
|
98
|
+
lambda t: Account(*t),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _decode_account(raw: JsonObj) -> ResultE[Maybe[Account]]:
|
|
103
|
+
return JsonUnfolder.optional(
|
|
104
|
+
raw,
|
|
105
|
+
"account",
|
|
106
|
+
lambda v: Unfolder.to_json(v).bind(_decode_account_obj),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _decode_person(raw: JsonObj) -> ResultE[Person]:
|
|
111
|
+
contact_id = JsonUnfolder.require(
|
|
112
|
+
raw,
|
|
113
|
+
"uuid",
|
|
114
|
+
lambda v: Unfolder.to_primitive(v).bind(JsonPrimitiveUnfolder.to_str),
|
|
115
|
+
).map(ContactId)
|
|
116
|
+
remote_id = JsonUnfolder.optional(
|
|
117
|
+
raw,
|
|
118
|
+
"remote_id",
|
|
119
|
+
lambda v: Unfolder.to_primitive(v).bind(JsonPrimitiveUnfolder.to_str),
|
|
120
|
+
)
|
|
121
|
+
email = JsonUnfolder.optional(
|
|
122
|
+
raw,
|
|
123
|
+
"email",
|
|
124
|
+
lambda v: Unfolder.to_primitive(v).bind(JsonPrimitiveUnfolder.to_str),
|
|
125
|
+
)
|
|
126
|
+
display_name = JsonUnfolder.optional(
|
|
127
|
+
raw,
|
|
128
|
+
"display_name",
|
|
129
|
+
lambda v: Unfolder.to_primitive(v).bind(JsonPrimitiveUnfolder.to_str),
|
|
130
|
+
)
|
|
131
|
+
first_seen_at = JsonUnfolder.optional(
|
|
132
|
+
raw,
|
|
133
|
+
"first_seen_at",
|
|
134
|
+
lambda v: Unfolder.to_primitive(v).bind(JsonPrimitiveUnfolder.to_str).bind(_iso_to_unix),
|
|
135
|
+
)
|
|
136
|
+
last_seen_at = JsonUnfolder.optional(
|
|
137
|
+
raw,
|
|
138
|
+
"last_seen_at",
|
|
139
|
+
lambda v: Unfolder.to_primitive(v).bind(JsonPrimitiveUnfolder.to_str).bind(_iso_to_unix),
|
|
140
|
+
)
|
|
141
|
+
account = _decode_account(raw)
|
|
142
|
+
group_1 = ResultSmash.smash_result_4(contact_id, remote_id, email, display_name)
|
|
143
|
+
group_2 = ResultSmash.smash_result_3(first_seen_at, last_seen_at, account)
|
|
144
|
+
return ResultSmash.smash_result_2(group_1, group_2).map(
|
|
145
|
+
lambda t: Person(*t[0], *t[1]),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _decode_json_response(resp: Response) -> ResultE[DataPage[Person]]:
|
|
150
|
+
return (
|
|
151
|
+
json_decode(resp)
|
|
152
|
+
.alt(cast_exception)
|
|
153
|
+
.bind(
|
|
154
|
+
lambda c: c.map(
|
|
155
|
+
lambda obj: decode_items(obj, _decode_person).bind(
|
|
156
|
+
lambda items: decode_next_cursor(obj).map(
|
|
157
|
+
lambda cursor: DataPage(items, cursor),
|
|
158
|
+
),
|
|
159
|
+
),
|
|
160
|
+
lambda lst: Result[DataPage[Person], Exception].failure(
|
|
161
|
+
Bug[Exception].new(
|
|
162
|
+
"_decode_json_response",
|
|
163
|
+
inspect.currentframe(),
|
|
164
|
+
ValueError(f"Expected json object but got list: {lst!r}"),
|
|
165
|
+
(),
|
|
166
|
+
),
|
|
167
|
+
),
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _get_contacts_page(
|
|
174
|
+
log: Logger,
|
|
175
|
+
client: HttpClient,
|
|
176
|
+
cursor: Maybe[str],
|
|
177
|
+
) -> Cmd[tuple[FrozenList[Person], Maybe[str]]]:
|
|
178
|
+
params: dict[str, JsonValue] = {
|
|
179
|
+
"page_length": JsonValue.from_primitive(JsonPrimitive.from_int(_MAX_PAGE_LENGTH)),
|
|
180
|
+
}
|
|
181
|
+
cursor_params = cursor.map(
|
|
182
|
+
lambda c: {
|
|
183
|
+
"page_cursor": JsonValue.from_primitive(JsonPrimitive.from_str(c)),
|
|
184
|
+
},
|
|
185
|
+
).value_or(dict[str, JsonValue]())
|
|
186
|
+
get_response = client.get(
|
|
187
|
+
relative_endpoint("v1/contacts"),
|
|
188
|
+
Params(FrozenDict({**params, **cursor_params})),
|
|
189
|
+
)
|
|
190
|
+
return Cmd[None].wrap_impure(
|
|
191
|
+
lambda: log.info("API: get_contacts_page()"),
|
|
192
|
+
) + with_retry_handler(get_response, 3).map(
|
|
193
|
+
lambda r: r.bind(_decode_json_response),
|
|
194
|
+
).map(
|
|
195
|
+
lambda r: Bug[DataPage[Person]].assume_success(
|
|
196
|
+
"_get_contacts_page",
|
|
197
|
+
inspect.currentframe(),
|
|
198
|
+
(),
|
|
199
|
+
r,
|
|
200
|
+
),
|
|
201
|
+
).map(
|
|
202
|
+
lambda page: (page.items, page.next_cursor),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def get_contacts(
|
|
207
|
+
log: Logger,
|
|
208
|
+
creds: Credentials,
|
|
209
|
+
) -> Stream[FrozenList[Person]]:
|
|
210
|
+
headers = UnfoldedFactory.from_dict({"Authorization": " ".join(["Bearer", creds.api_key])})
|
|
211
|
+
client = HttpClientFactory.new_client(None, headers, None)
|
|
212
|
+
return cursor_pagination(lambda cursor: _get_contacts_page(log, client, cursor))
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import logging
|
|
3
|
+
from logging import (
|
|
4
|
+
Logger,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
from fa_purity import (
|
|
8
|
+
Cmd,
|
|
9
|
+
FrozenDict,
|
|
10
|
+
FrozenList,
|
|
11
|
+
Maybe,
|
|
12
|
+
Result,
|
|
13
|
+
ResultE,
|
|
14
|
+
Stream,
|
|
15
|
+
cast_exception,
|
|
16
|
+
)
|
|
17
|
+
from fa_purity.json import (
|
|
18
|
+
JsonObj,
|
|
19
|
+
JsonPrimitive,
|
|
20
|
+
JsonPrimitiveUnfolder,
|
|
21
|
+
JsonUnfolder,
|
|
22
|
+
JsonValue,
|
|
23
|
+
UnfoldedFactory,
|
|
24
|
+
Unfolder,
|
|
25
|
+
)
|
|
26
|
+
from fluidattacks_etl_utils.bug import (
|
|
27
|
+
Bug,
|
|
28
|
+
)
|
|
29
|
+
from fluidattacks_etl_utils.paginate import (
|
|
30
|
+
cursor_pagination,
|
|
31
|
+
)
|
|
32
|
+
from pure_requests.basic import (
|
|
33
|
+
HttpClient,
|
|
34
|
+
HttpClientFactory,
|
|
35
|
+
Params,
|
|
36
|
+
)
|
|
37
|
+
from pure_requests.response import (
|
|
38
|
+
json_decode,
|
|
39
|
+
)
|
|
40
|
+
from requests import (
|
|
41
|
+
Response,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
from fluidattacks_refiner_sdk.auth import (
|
|
45
|
+
Credentials,
|
|
46
|
+
)
|
|
47
|
+
from fluidattacks_refiner_sdk.client._common import (
|
|
48
|
+
DataPage,
|
|
49
|
+
decode_items,
|
|
50
|
+
decode_next_cursor,
|
|
51
|
+
with_retry_handler,
|
|
52
|
+
)
|
|
53
|
+
from fluidattacks_refiner_sdk.core import (
|
|
54
|
+
Form,
|
|
55
|
+
FormId,
|
|
56
|
+
FormName,
|
|
57
|
+
relative_endpoint,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
LOG = logging.getLogger(__name__)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _decode_form(raw: JsonObj) -> ResultE[Form]:
|
|
64
|
+
form_id = JsonUnfolder.require(
|
|
65
|
+
raw,
|
|
66
|
+
"uuid",
|
|
67
|
+
lambda v: Unfolder.to_primitive(v).bind(JsonPrimitiveUnfolder.to_str),
|
|
68
|
+
)
|
|
69
|
+
name = JsonUnfolder.require(
|
|
70
|
+
raw,
|
|
71
|
+
"name",
|
|
72
|
+
lambda v: Unfolder.to_primitive(v).bind(JsonPrimitiveUnfolder.to_str),
|
|
73
|
+
)
|
|
74
|
+
return form_id.bind(
|
|
75
|
+
lambda fid: name.map(
|
|
76
|
+
lambda n: Form(id=FormId(fid), name=FormName(n)),
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _decode_json_response(resp: Response) -> ResultE[DataPage[Form]]:
|
|
82
|
+
return (
|
|
83
|
+
json_decode(resp)
|
|
84
|
+
.alt(cast_exception)
|
|
85
|
+
.bind(
|
|
86
|
+
lambda c: c.map(
|
|
87
|
+
lambda obj: decode_items(obj, _decode_form).bind(
|
|
88
|
+
lambda items: decode_next_cursor(obj).map(
|
|
89
|
+
lambda cursor: DataPage(items, cursor),
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
lambda _: Result[DataPage[Form], Exception].failure(
|
|
93
|
+
Bug[Exception].new(
|
|
94
|
+
"_decode_json_response",
|
|
95
|
+
inspect.currentframe(),
|
|
96
|
+
ValueError("Expected json object but got list"),
|
|
97
|
+
(),
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _get_forms_page(
|
|
106
|
+
log: Logger,
|
|
107
|
+
client: HttpClient,
|
|
108
|
+
cursor: Maybe[str],
|
|
109
|
+
) -> Cmd[tuple[FrozenList[Form], Maybe[str]]]:
|
|
110
|
+
params: dict[str, JsonValue] = {
|
|
111
|
+
"page_length": JsonValue.from_primitive(JsonPrimitive.from_int(100)),
|
|
112
|
+
}
|
|
113
|
+
cursor_params = cursor.map(
|
|
114
|
+
lambda c: {
|
|
115
|
+
"page_cursor": JsonValue.from_primitive(JsonPrimitive.from_str(c)),
|
|
116
|
+
},
|
|
117
|
+
).value_or(dict[str, JsonValue]())
|
|
118
|
+
get_response = client.get(
|
|
119
|
+
relative_endpoint("v1/forms"),
|
|
120
|
+
Params(FrozenDict({**params, **cursor_params})),
|
|
121
|
+
)
|
|
122
|
+
return Cmd[None].wrap_impure(
|
|
123
|
+
lambda: log.info("API: get_forms_page()"),
|
|
124
|
+
) + with_retry_handler(get_response, 3).map(
|
|
125
|
+
lambda r: r.bind(_decode_json_response),
|
|
126
|
+
).map(
|
|
127
|
+
lambda r: Bug[DataPage[Form]].assume_success(
|
|
128
|
+
"_get_forms_page",
|
|
129
|
+
inspect.currentframe(),
|
|
130
|
+
(),
|
|
131
|
+
r,
|
|
132
|
+
),
|
|
133
|
+
).map(
|
|
134
|
+
lambda page: (page.items, page.next_cursor),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def get_forms(
|
|
139
|
+
log: Logger,
|
|
140
|
+
creds: Credentials,
|
|
141
|
+
) -> Stream[FrozenList[Form]]:
|
|
142
|
+
headers = UnfoldedFactory.from_dict({"Authorization": " ".join(["Bearer", creds.api_key])})
|
|
143
|
+
client = HttpClientFactory.new_client(None, headers, None)
|
|
144
|
+
return cursor_pagination(lambda cursor: _get_forms_page(log, client, cursor))
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import logging
|
|
3
|
+
from logging import (
|
|
4
|
+
Logger,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
from fa_purity import (
|
|
8
|
+
Cmd,
|
|
9
|
+
FrozenDict,
|
|
10
|
+
FrozenList,
|
|
11
|
+
Result,
|
|
12
|
+
ResultE,
|
|
13
|
+
cast_exception,
|
|
14
|
+
)
|
|
15
|
+
from fa_purity.json import (
|
|
16
|
+
JsonObj,
|
|
17
|
+
JsonPrimitive,
|
|
18
|
+
JsonPrimitiveUnfolder,
|
|
19
|
+
JsonUnfolder,
|
|
20
|
+
JsonValue,
|
|
21
|
+
UnfoldedFactory,
|
|
22
|
+
Unfolder,
|
|
23
|
+
)
|
|
24
|
+
from fluidattacks_etl_utils.bug import (
|
|
25
|
+
Bug,
|
|
26
|
+
)
|
|
27
|
+
from pure_requests.basic import (
|
|
28
|
+
HttpClientFactory,
|
|
29
|
+
Params,
|
|
30
|
+
)
|
|
31
|
+
from pure_requests.response import (
|
|
32
|
+
json_decode,
|
|
33
|
+
)
|
|
34
|
+
from requests import (
|
|
35
|
+
Response,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
from fluidattacks_refiner_sdk.auth import (
|
|
39
|
+
Credentials,
|
|
40
|
+
)
|
|
41
|
+
from fluidattacks_refiner_sdk.client._common import (
|
|
42
|
+
with_retry_handler,
|
|
43
|
+
)
|
|
44
|
+
from fluidattacks_refiner_sdk.core import (
|
|
45
|
+
NpsMetrics,
|
|
46
|
+
relative_endpoint,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
LOG = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _decode_count(obj: JsonObj, key: str) -> ResultE[int]:
|
|
53
|
+
return JsonUnfolder.require(
|
|
54
|
+
obj,
|
|
55
|
+
key,
|
|
56
|
+
lambda v: Unfolder.to_primitive(v).bind(
|
|
57
|
+
lambda p: JsonPrimitiveUnfolder.to_int(p).lash(
|
|
58
|
+
lambda _: JsonPrimitiveUnfolder.to_float(p).map(int),
|
|
59
|
+
),
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _decode_nps_obj(obj: JsonObj) -> ResultE[NpsMetrics]:
|
|
65
|
+
nps = JsonUnfolder.require(
|
|
66
|
+
obj,
|
|
67
|
+
"nps",
|
|
68
|
+
lambda v: Unfolder.to_primitive(v).bind(
|
|
69
|
+
lambda p: JsonPrimitiveUnfolder.to_int(p).lash(
|
|
70
|
+
lambda _: JsonPrimitiveUnfolder.to_float(p).map(int),
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
count = _decode_count(obj, "count")
|
|
75
|
+
data_obj = JsonUnfolder.require(obj, "data", Unfolder.to_json)
|
|
76
|
+
promoters_count = data_obj.bind(lambda d: _decode_count(d, "promoters"))
|
|
77
|
+
passives_count = data_obj.bind(lambda d: _decode_count(d, "passives"))
|
|
78
|
+
detractors_count = data_obj.bind(lambda d: _decode_count(d, "detractors"))
|
|
79
|
+
return nps.bind(
|
|
80
|
+
lambda n: count.bind(
|
|
81
|
+
lambda c: promoters_count.bind(
|
|
82
|
+
lambda pc: passives_count.bind(
|
|
83
|
+
lambda pac: detractors_count.map(
|
|
84
|
+
lambda dc: NpsMetrics(
|
|
85
|
+
nps=n,
|
|
86
|
+
promoter_count=pc,
|
|
87
|
+
promoter_percent=pc / c * 100 if c else 0.0,
|
|
88
|
+
passive_count=pac,
|
|
89
|
+
passive_percent=pac / c * 100 if c else 0.0,
|
|
90
|
+
detractor_count=dc,
|
|
91
|
+
detractor_percent=dc / c * 100 if c else 0.0,
|
|
92
|
+
response_count=c,
|
|
93
|
+
),
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _decode_nps_metrics(resp: Response) -> ResultE[NpsMetrics]:
|
|
102
|
+
def _decode_list(lst: FrozenList[JsonObj]) -> ResultE[NpsMetrics]:
|
|
103
|
+
if not lst:
|
|
104
|
+
return Result[NpsMetrics, Exception].failure(ValueError("Empty NPS metrics response"))
|
|
105
|
+
return _decode_nps_obj(lst[0])
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
json_decode(resp).alt(cast_exception).bind(lambda c: c.map(_decode_nps_obj, _decode_list))
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def get_nps_metrics(
|
|
113
|
+
log: Logger,
|
|
114
|
+
creds: Credentials,
|
|
115
|
+
) -> Cmd[NpsMetrics]:
|
|
116
|
+
headers = UnfoldedFactory.from_dict({"Authorization": " ".join(["Bearer", creds.api_key])})
|
|
117
|
+
client = HttpClientFactory.new_client(None, headers, None)
|
|
118
|
+
get_response = client.get(
|
|
119
|
+
relative_endpoint("v1/reporting"),
|
|
120
|
+
Params(
|
|
121
|
+
FrozenDict(
|
|
122
|
+
{
|
|
123
|
+
"type": JsonValue.from_primitive(JsonPrimitive.from_str("nps")),
|
|
124
|
+
},
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
return Cmd[None].wrap_impure(lambda: log.info("API: get_nps_metrics()")) + with_retry_handler(
|
|
129
|
+
get_response,
|
|
130
|
+
3,
|
|
131
|
+
).map(
|
|
132
|
+
lambda r: r.bind(_decode_nps_metrics),
|
|
133
|
+
).map(
|
|
134
|
+
lambda r: Bug[NpsMetrics].assume_success(
|
|
135
|
+
"get_nps_metrics",
|
|
136
|
+
inspect.currentframe(),
|
|
137
|
+
(),
|
|
138
|
+
r,
|
|
139
|
+
),
|
|
140
|
+
)
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import logging
|
|
3
|
+
from logging import (
|
|
4
|
+
Logger,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
from dateutil.parser import parse as parse_dt
|
|
8
|
+
from fa_purity import (
|
|
9
|
+
Cmd,
|
|
10
|
+
FrozenDict,
|
|
11
|
+
FrozenList,
|
|
12
|
+
Maybe,
|
|
13
|
+
Result,
|
|
14
|
+
ResultE,
|
|
15
|
+
ResultSmash,
|
|
16
|
+
Stream,
|
|
17
|
+
cast_exception,
|
|
18
|
+
)
|
|
19
|
+
from fa_purity.json import (
|
|
20
|
+
JsonObj,
|
|
21
|
+
JsonPrimitive,
|
|
22
|
+
JsonPrimitiveUnfolder,
|
|
23
|
+
JsonUnfolder,
|
|
24
|
+
JsonValue,
|
|
25
|
+
UnfoldedFactory,
|
|
26
|
+
Unfolder,
|
|
27
|
+
)
|
|
28
|
+
from fluidattacks_etl_utils.bug import (
|
|
29
|
+
Bug,
|
|
30
|
+
)
|
|
31
|
+
from fluidattacks_etl_utils.paginate import (
|
|
32
|
+
cursor_pagination,
|
|
33
|
+
)
|
|
34
|
+
from pure_requests.basic import (
|
|
35
|
+
HttpClient,
|
|
36
|
+
HttpClientFactory,
|
|
37
|
+
Params,
|
|
38
|
+
)
|
|
39
|
+
from pure_requests.response import (
|
|
40
|
+
json_decode,
|
|
41
|
+
)
|
|
42
|
+
from requests import (
|
|
43
|
+
Response,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
from fluidattacks_refiner_sdk.auth import (
|
|
47
|
+
Credentials,
|
|
48
|
+
)
|
|
49
|
+
from fluidattacks_refiner_sdk.client._common import (
|
|
50
|
+
DataPage,
|
|
51
|
+
decode_items,
|
|
52
|
+
decode_next_cursor,
|
|
53
|
+
with_retry_handler,
|
|
54
|
+
)
|
|
55
|
+
from fluidattacks_refiner_sdk.core import (
|
|
56
|
+
Account,
|
|
57
|
+
AccountDomain,
|
|
58
|
+
AccountId,
|
|
59
|
+
Contact,
|
|
60
|
+
ContactId,
|
|
61
|
+
FormId,
|
|
62
|
+
FormName,
|
|
63
|
+
ResponseId,
|
|
64
|
+
SurveyResponse,
|
|
65
|
+
relative_endpoint,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
LOG = logging.getLogger(__name__)
|
|
69
|
+
|
|
70
|
+
_MAX_PAGE_LENGTH = 1000
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _iso_to_unix(iso: str) -> ResultE[int]:
|
|
74
|
+
try:
|
|
75
|
+
return Result[int, Exception].success(int(parse_dt(iso).timestamp()))
|
|
76
|
+
except (ValueError, OverflowError) as e:
|
|
77
|
+
return Result[int, Exception].failure(e)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _decode_int_value(v: JsonValue) -> ResultE[int]:
|
|
81
|
+
return Unfolder.to_primitive(v).bind(JsonPrimitiveUnfolder.to_int)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _decode_str_value(v: JsonValue) -> ResultE[str]:
|
|
85
|
+
return Unfolder.to_primitive(v).bind(JsonPrimitiveUnfolder.to_str)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _decode_account_obj(obj: JsonObj) -> ResultE[Account]:
|
|
89
|
+
return JsonUnfolder.require(
|
|
90
|
+
obj,
|
|
91
|
+
"uuid",
|
|
92
|
+
lambda vv: Unfolder.to_primitive(vv).bind(JsonPrimitiveUnfolder.to_str),
|
|
93
|
+
).map(
|
|
94
|
+
lambda acc_id: Account(
|
|
95
|
+
id=AccountId(acc_id),
|
|
96
|
+
remote_id=Maybe[str].empty(),
|
|
97
|
+
display_name=Maybe[str].empty(),
|
|
98
|
+
domain=Maybe[AccountDomain].empty(),
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _decode_account(raw: JsonObj) -> ResultE[Maybe[Account]]:
|
|
104
|
+
return JsonUnfolder.optional(
|
|
105
|
+
raw,
|
|
106
|
+
"account",
|
|
107
|
+
lambda v: Unfolder.to_json(v).bind(_decode_account_obj),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _decode_contact(raw: JsonObj) -> ResultE[Contact]:
|
|
112
|
+
contact_id = JsonUnfolder.require(
|
|
113
|
+
raw,
|
|
114
|
+
"uuid",
|
|
115
|
+
lambda v: Unfolder.to_primitive(v).bind(JsonPrimitiveUnfolder.to_str),
|
|
116
|
+
).map(ContactId)
|
|
117
|
+
email = JsonUnfolder.optional(
|
|
118
|
+
raw,
|
|
119
|
+
"email",
|
|
120
|
+
lambda v: Unfolder.to_primitive(v).bind(JsonPrimitiveUnfolder.to_str),
|
|
121
|
+
)
|
|
122
|
+
display_name = JsonUnfolder.optional(
|
|
123
|
+
raw,
|
|
124
|
+
"display_name",
|
|
125
|
+
lambda v: Unfolder.to_primitive(v).bind(JsonPrimitiveUnfolder.to_str),
|
|
126
|
+
)
|
|
127
|
+
account = _decode_account(raw)
|
|
128
|
+
return ResultSmash.smash_result_4(contact_id, email, display_name, account).map(
|
|
129
|
+
lambda t: Contact(*t),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _find_nps_score(data: JsonObj) -> Maybe[int]:
|
|
134
|
+
for key in ("nps", "NPS", "score", "nps_score"):
|
|
135
|
+
found: Maybe[int] = JsonUnfolder.optional(
|
|
136
|
+
data,
|
|
137
|
+
key,
|
|
138
|
+
_decode_int_value,
|
|
139
|
+
).value_or(Maybe[int].empty())
|
|
140
|
+
if found != Maybe[int].empty():
|
|
141
|
+
return found
|
|
142
|
+
return Maybe[int].empty()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _find_comment(data: JsonObj) -> Maybe[str]:
|
|
146
|
+
for key in ("comment", "feedback", "message", "text"):
|
|
147
|
+
found: Maybe[str] = JsonUnfolder.optional(
|
|
148
|
+
data,
|
|
149
|
+
key,
|
|
150
|
+
_decode_str_value,
|
|
151
|
+
).value_or(Maybe[str].empty())
|
|
152
|
+
if found != Maybe[str].empty():
|
|
153
|
+
return found
|
|
154
|
+
return Maybe[str].empty()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _decode_response(raw: JsonObj) -> ResultE[SurveyResponse]:
|
|
158
|
+
resp_id = JsonUnfolder.require(
|
|
159
|
+
raw,
|
|
160
|
+
"uuid",
|
|
161
|
+
lambda v: Unfolder.to_primitive(v).bind(JsonPrimitiveUnfolder.to_str),
|
|
162
|
+
).map(ResponseId)
|
|
163
|
+
completed_at = JsonUnfolder.optional(
|
|
164
|
+
raw,
|
|
165
|
+
"completed_at",
|
|
166
|
+
lambda v: Unfolder.to_primitive(v).bind(JsonPrimitiveUnfolder.to_str).bind(_iso_to_unix),
|
|
167
|
+
)
|
|
168
|
+
received_at = JsonUnfolder.optional(
|
|
169
|
+
raw,
|
|
170
|
+
"received_at",
|
|
171
|
+
lambda v: Unfolder.to_primitive(v).bind(JsonPrimitiveUnfolder.to_str).bind(_iso_to_unix),
|
|
172
|
+
)
|
|
173
|
+
dismissed_at = JsonUnfolder.optional(
|
|
174
|
+
raw,
|
|
175
|
+
"dismissed_at",
|
|
176
|
+
lambda v: Unfolder.to_primitive(v).bind(JsonPrimitiveUnfolder.to_str).bind(_iso_to_unix),
|
|
177
|
+
)
|
|
178
|
+
form_id = JsonUnfolder.require(
|
|
179
|
+
raw,
|
|
180
|
+
"form",
|
|
181
|
+
lambda v: Unfolder.to_json(v).bind(
|
|
182
|
+
lambda obj: JsonUnfolder.require(
|
|
183
|
+
obj,
|
|
184
|
+
"uuid",
|
|
185
|
+
lambda vv: Unfolder.to_primitive(vv).bind(JsonPrimitiveUnfolder.to_str),
|
|
186
|
+
),
|
|
187
|
+
),
|
|
188
|
+
).map(FormId)
|
|
189
|
+
form_name = JsonUnfolder.require(
|
|
190
|
+
raw,
|
|
191
|
+
"form",
|
|
192
|
+
lambda v: Unfolder.to_json(v).bind(
|
|
193
|
+
lambda obj: JsonUnfolder.require(
|
|
194
|
+
obj,
|
|
195
|
+
"name",
|
|
196
|
+
lambda vv: Unfolder.to_primitive(vv).bind(JsonPrimitiveUnfolder.to_str),
|
|
197
|
+
),
|
|
198
|
+
),
|
|
199
|
+
).map(FormName)
|
|
200
|
+
contact = JsonUnfolder.require(
|
|
201
|
+
raw,
|
|
202
|
+
"contact",
|
|
203
|
+
lambda v: Unfolder.to_json(v).bind(_decode_contact),
|
|
204
|
+
)
|
|
205
|
+
nps_score = JsonUnfolder.optional(
|
|
206
|
+
raw,
|
|
207
|
+
"data",
|
|
208
|
+
lambda v: Unfolder.to_json(v).map(_find_nps_score),
|
|
209
|
+
).map(lambda m: m.bind(lambda x: x))
|
|
210
|
+
comment = JsonUnfolder.optional(
|
|
211
|
+
raw,
|
|
212
|
+
"data",
|
|
213
|
+
lambda v: Unfolder.to_json(v).map(_find_comment),
|
|
214
|
+
).map(lambda m: m.bind(lambda x: x))
|
|
215
|
+
raw_data: ResultE[Maybe[str]] = Result[Maybe[str], Exception].success(Maybe[str].empty())
|
|
216
|
+
group_1 = ResultSmash.smash_result_5(resp_id, completed_at, received_at, dismissed_at, form_id)
|
|
217
|
+
group_2 = ResultSmash.smash_result_5(form_name, contact, nps_score, comment, raw_data)
|
|
218
|
+
return ResultSmash.smash_result_2(group_1, group_2).map(
|
|
219
|
+
lambda t: SurveyResponse(*t[0], *t[1]),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _decode_json_response(resp: Response) -> ResultE[DataPage[SurveyResponse]]:
|
|
224
|
+
return (
|
|
225
|
+
json_decode(resp)
|
|
226
|
+
.alt(cast_exception)
|
|
227
|
+
.bind(
|
|
228
|
+
lambda c: c.map(
|
|
229
|
+
lambda obj: decode_items(obj, _decode_response).bind(
|
|
230
|
+
lambda items: decode_next_cursor(obj).map(
|
|
231
|
+
lambda cursor: DataPage(items, cursor),
|
|
232
|
+
),
|
|
233
|
+
),
|
|
234
|
+
lambda lst: Result[DataPage[SurveyResponse], Exception].failure(
|
|
235
|
+
Bug[Exception].new(
|
|
236
|
+
"_decode_json_response",
|
|
237
|
+
inspect.currentframe(),
|
|
238
|
+
ValueError(f"Expected json object but got list: {lst!r}"),
|
|
239
|
+
(),
|
|
240
|
+
),
|
|
241
|
+
),
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _get_responses_page(
|
|
248
|
+
log: Logger,
|
|
249
|
+
client: HttpClient,
|
|
250
|
+
form_id: FormId,
|
|
251
|
+
cursor: Maybe[str],
|
|
252
|
+
) -> Cmd[tuple[FrozenList[SurveyResponse], Maybe[str]]]:
|
|
253
|
+
params: dict[str, JsonValue] = {
|
|
254
|
+
"page_length": JsonValue.from_primitive(JsonPrimitive.from_int(_MAX_PAGE_LENGTH)),
|
|
255
|
+
"form_uuid": JsonValue.from_primitive(JsonPrimitive.from_str(form_id.form)),
|
|
256
|
+
}
|
|
257
|
+
cursor_params = cursor.map(
|
|
258
|
+
lambda c: {
|
|
259
|
+
"page_cursor": JsonValue.from_primitive(JsonPrimitive.from_str(c)),
|
|
260
|
+
},
|
|
261
|
+
).value_or(dict[str, JsonValue]())
|
|
262
|
+
get_response = client.get(
|
|
263
|
+
relative_endpoint("v1/responses"),
|
|
264
|
+
Params(FrozenDict({**params, **cursor_params})),
|
|
265
|
+
)
|
|
266
|
+
return Cmd[None].wrap_impure(
|
|
267
|
+
lambda: log.info("API: get_responses_page(form=%s)", form_id.form),
|
|
268
|
+
) + with_retry_handler(get_response, 3).map(
|
|
269
|
+
lambda r: r.bind(_decode_json_response),
|
|
270
|
+
).map(
|
|
271
|
+
lambda r: Bug[DataPage[SurveyResponse]].assume_success(
|
|
272
|
+
"_get_responses_page",
|
|
273
|
+
inspect.currentframe(),
|
|
274
|
+
(),
|
|
275
|
+
r,
|
|
276
|
+
),
|
|
277
|
+
).map(
|
|
278
|
+
lambda page: (page.items, page.next_cursor),
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def get_responses(
|
|
283
|
+
log: Logger,
|
|
284
|
+
creds: Credentials,
|
|
285
|
+
form_id: FormId,
|
|
286
|
+
) -> Stream[FrozenList[SurveyResponse]]:
|
|
287
|
+
headers = UnfoldedFactory.from_dict({"Authorization": " ".join(["Bearer", creds.api_key])})
|
|
288
|
+
client = HttpClientFactory.new_client(None, headers, None)
|
|
289
|
+
return cursor_pagination(lambda cursor: _get_responses_page(log, client, form_id, cursor))
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from collections.abc import (
|
|
2
|
+
Callable,
|
|
3
|
+
)
|
|
4
|
+
from dataclasses import (
|
|
5
|
+
dataclass,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from fa_purity import (
|
|
9
|
+
Cmd,
|
|
10
|
+
FrozenList,
|
|
11
|
+
Maybe,
|
|
12
|
+
Stream,
|
|
13
|
+
)
|
|
14
|
+
from pure_requests.basic import (
|
|
15
|
+
Endpoint,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
REFINER_API = Endpoint("https://api.refiner.io")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def relative_endpoint(relative: str) -> Endpoint:
|
|
22
|
+
return Endpoint(REFINER_API.raw.rstrip("/") + "/" + relative.lstrip("/"))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class FormId:
|
|
27
|
+
form: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class FormName:
|
|
32
|
+
name: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class AccountId:
|
|
37
|
+
account: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class AccountName:
|
|
42
|
+
name: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class AccountDomain:
|
|
47
|
+
domain: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class Account:
|
|
52
|
+
id: AccountId
|
|
53
|
+
remote_id: Maybe[str]
|
|
54
|
+
display_name: Maybe[str]
|
|
55
|
+
domain: Maybe[AccountDomain]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class ContactId:
|
|
60
|
+
contact: str
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class Contact:
|
|
65
|
+
id: ContactId
|
|
66
|
+
email: Maybe[str]
|
|
67
|
+
display_name: Maybe[str]
|
|
68
|
+
account: Maybe[Account]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class ResponseId:
|
|
73
|
+
response: str
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class SurveyResponse:
|
|
78
|
+
id: ResponseId
|
|
79
|
+
completed_at: Maybe[int]
|
|
80
|
+
received_at: Maybe[int]
|
|
81
|
+
dismissed_at: Maybe[int]
|
|
82
|
+
form_id: FormId
|
|
83
|
+
form_name: FormName
|
|
84
|
+
contact: Contact
|
|
85
|
+
nps_score: Maybe[int]
|
|
86
|
+
comment: Maybe[str]
|
|
87
|
+
raw_data: Maybe[str]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True)
|
|
91
|
+
class Person:
|
|
92
|
+
id: ContactId
|
|
93
|
+
remote_id: Maybe[str]
|
|
94
|
+
email: Maybe[str]
|
|
95
|
+
display_name: Maybe[str]
|
|
96
|
+
first_seen_at: Maybe[int]
|
|
97
|
+
last_seen_at: Maybe[int]
|
|
98
|
+
account: Maybe[Account]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True)
|
|
102
|
+
class NpsMetrics:
|
|
103
|
+
nps: int
|
|
104
|
+
promoter_count: int
|
|
105
|
+
promoter_percent: float
|
|
106
|
+
passive_count: int
|
|
107
|
+
passive_percent: float
|
|
108
|
+
detractor_count: int
|
|
109
|
+
detractor_percent: float
|
|
110
|
+
response_count: int
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True)
|
|
114
|
+
class Form:
|
|
115
|
+
id: FormId
|
|
116
|
+
name: FormName
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True)
|
|
120
|
+
class ApiClient:
|
|
121
|
+
get_forms: Stream[FrozenList[Form]]
|
|
122
|
+
get_responses: Callable[[FormId], Stream[FrozenList[SurveyResponse]]]
|
|
123
|
+
get_contacts: Stream[FrozenList[Person]]
|
|
124
|
+
get_nps_metrics: Cmd[NpsMetrics]
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fluidattacks_refiner_sdk
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: refiner SDK
|
|
5
|
+
Author-email: Product Team <development@fluidattacks.com>
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: click >=8.3.0, <9.0.0
|
|
8
|
+
Requires-Dist: fluidattacks-etl-utils >=5.0.0, <6.0.0
|
|
9
|
+
Requires-Dist: fa-purity >=2.5.2, <3.0.0
|
|
10
|
+
Requires-Dist: pure-requests >=3.0.0, <4.0.0
|
|
11
|
+
Requires-Dist: python-dateutil >=2.8.0, <3.0.0
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
fluidattacks_refiner_sdk/__init__.py,sha256=rnObPjuBcEStqSO0S6gsdS_ot8ITOQjVj_-P1LUUYpg,22
|
|
2
|
+
fluidattacks_refiner_sdk/core.py,sha256=cq_RkGp_ngAR7GRX94Xwp67IALK4dBVE435g8ZAqIII,2156
|
|
3
|
+
fluidattacks_refiner_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
fluidattacks_refiner_sdk/auth/__init__.py,sha256=N28zBB_AOpt43qbLf1Duy56dj4DQ-Z7WBAMBEc8erRA,74
|
|
5
|
+
fluidattacks_refiner_sdk/auth/_core.py,sha256=dJaZZCfsc8ZW56jXPSZREmhNw84R5u-6Vrrza_OjCNY,136
|
|
6
|
+
fluidattacks_refiner_sdk/client/__init__.py,sha256=pZYeiKSpC71NLlCATvAavefjj6lEEO4_icivwPG0enk,822
|
|
7
|
+
fluidattacks_refiner_sdk/client/_common.py,sha256=AGfCg5q0hWBjj_Ep7gZMakRUFI1-HIJ3cAUmV6DxPiw,3101
|
|
8
|
+
fluidattacks_refiner_sdk/client/_get_contacts.py,sha256=1KZzSiNL0TRMDwu1I7tFpF0rk-qP7zvbe74-cBE-MM8,5974
|
|
9
|
+
fluidattacks_refiner_sdk/client/_get_forms.py,sha256=ZT_lrxKd-DDfgNUxY832e8MBEvYJoFG13YYHxvMaQzE,3614
|
|
10
|
+
fluidattacks_refiner_sdk/client/_get_reporting.py,sha256=80lOOf1MdVw2OTDdlLOXxDFOicHJehC5Je7AgDqz1xw,3900
|
|
11
|
+
fluidattacks_refiner_sdk/client/_get_responses.py,sha256=omhdtzXTL_xiG9qc7MfMw5d7QIRvklJvkAMjflIJN54,8424
|
|
12
|
+
fluidattacks_refiner_sdk-0.1.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
13
|
+
fluidattacks_refiner_sdk-0.1.1.dist-info/METADATA,sha256=R03A18lk4pEkiuZlt6BsjHXS5IqW5UcXCoKNItq_Dyk,395
|
|
14
|
+
fluidattacks_refiner_sdk-0.1.1.dist-info/RECORD,,
|