dailybot-cli 3.4.0__tar.gz → 3.5.1__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.
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/PKG-INFO +1 -1
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/api_client.py +123 -40
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/form.py +192 -7
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/public_api_helpers.py +12 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/user_scoped_actions.py +29 -2
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli.egg-info/PKG-INFO +1 -1
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/pyproject.toml +1 -1
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/LICENSE +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/README.md +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/__init__.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/__init__.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/agent.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/ask.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/auth.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/authoring_helpers.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/channels.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/chat.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/checkin.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/config.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/conversation.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/hook.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/identity.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/interactive.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/interactive_chat.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/kudos.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/query_options.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/status.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/team.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/uninstall.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/update.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/upgrade.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/user.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/version.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/workflow.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/config.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/display.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/install_method.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/ledger.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/main.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/tui/__init__.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/tui/app.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/tui/commands.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/tui/conversation.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/tui/intents.py +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli.egg-info/SOURCES.txt +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli.egg-info/dependency_links.txt +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli.egg-info/entry_points.txt +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli.egg-info/requires.txt +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli.egg-info/top_level.txt +0 -0
- {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dailybot-cli
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.5.1
|
|
4
4
|
Summary: Dailybot CLI - The command-line bridge between humans and agents. Progress reports, observability, and workflow automation for modern teams across Slack, Google Chat, Discord, MS Teams, and more.
|
|
5
5
|
Author-email: Dailybot <support@dailybot.com>
|
|
6
6
|
License: MIT
|
|
@@ -156,21 +156,77 @@ class DailyBotClient:
|
|
|
156
156
|
return headers
|
|
157
157
|
|
|
158
158
|
def _agent_headers(self) -> dict[str, str]:
|
|
159
|
-
"""Build headers for agent authentication
|
|
159
|
+
"""Build headers for agent authentication.
|
|
160
|
+
|
|
161
|
+
Uses the same priority as ``_headers()`` — Bearer first, API key
|
|
162
|
+
second — so that all endpoints behave consistently. The server
|
|
163
|
+
accepts both on every ``/v1/agent*`` endpoint.
|
|
164
|
+
"""
|
|
160
165
|
headers: dict[str, str] = {
|
|
161
166
|
"Content-Type": "application/json",
|
|
162
167
|
"Accept": "application/json",
|
|
163
168
|
}
|
|
164
|
-
if self.
|
|
165
|
-
headers["X-API-KEY"] = self.api_key
|
|
166
|
-
self._agent_auth_mode = "api_key"
|
|
167
|
-
elif self.token:
|
|
169
|
+
if self.token:
|
|
168
170
|
headers["Authorization"] = f"Bearer {self.token}"
|
|
169
171
|
self._agent_auth_mode = "bearer"
|
|
172
|
+
elif self.api_key:
|
|
173
|
+
headers["X-API-KEY"] = self.api_key
|
|
174
|
+
self._agent_auth_mode = "api_key"
|
|
170
175
|
else:
|
|
171
176
|
self._agent_auth_mode = None
|
|
172
177
|
return headers
|
|
173
178
|
|
|
179
|
+
def _alt_auth_headers(self) -> dict[str, str] | None:
|
|
180
|
+
"""Build headers using the alternative credential for a 401 retry.
|
|
181
|
+
|
|
182
|
+
If the primary was Bearer, tries API key; if the primary was API key,
|
|
183
|
+
tries Bearer. Returns ``None`` when no alternative is available.
|
|
184
|
+
"""
|
|
185
|
+
headers: dict[str, str] = {
|
|
186
|
+
"Content-Type": "application/json",
|
|
187
|
+
"Accept": "application/json",
|
|
188
|
+
}
|
|
189
|
+
if self._agent_auth_mode == "bearer" and self.api_key:
|
|
190
|
+
headers["X-API-KEY"] = self.api_key
|
|
191
|
+
self._agent_auth_mode = "api_key"
|
|
192
|
+
return headers
|
|
193
|
+
if self._agent_auth_mode == "api_key" and self.token:
|
|
194
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
195
|
+
self._agent_auth_mode = "bearer"
|
|
196
|
+
return headers
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
def _agent_request(
|
|
200
|
+
self,
|
|
201
|
+
method: str,
|
|
202
|
+
url: str,
|
|
203
|
+
*,
|
|
204
|
+
json: dict[str, Any] | None = None,
|
|
205
|
+
params: dict[str, Any] | None = None,
|
|
206
|
+
) -> httpx.Response:
|
|
207
|
+
"""Execute an agent-authenticated request with automatic alt-credential retry.
|
|
208
|
+
|
|
209
|
+
Tries with ``_agent_headers()`` (Bearer preferred, API key fallback).
|
|
210
|
+
If the server returns 401 and an alternative credential is available,
|
|
211
|
+
retries once with it. This covers both directions: expired Bearer
|
|
212
|
+
retried with API key, and stale API key retried with Bearer.
|
|
213
|
+
"""
|
|
214
|
+
kwargs: dict[str, Any] = {"headers": self._agent_headers(), "timeout": self.timeout}
|
|
215
|
+
if json is not None:
|
|
216
|
+
kwargs["json"] = json
|
|
217
|
+
if params is not None:
|
|
218
|
+
kwargs["params"] = params
|
|
219
|
+
|
|
220
|
+
response: httpx.Response = httpx.request(method, url, **kwargs)
|
|
221
|
+
|
|
222
|
+
if response.status_code == 401:
|
|
223
|
+
alt: dict[str, str] | None = self._alt_auth_headers()
|
|
224
|
+
if alt is not None:
|
|
225
|
+
kwargs["headers"] = alt
|
|
226
|
+
response = httpx.request(method, url, **kwargs)
|
|
227
|
+
|
|
228
|
+
return response
|
|
229
|
+
|
|
174
230
|
def _handle_response(self, response: httpx.Response) -> dict[str, Any]:
|
|
175
231
|
"""Parse API response and raise on errors."""
|
|
176
232
|
if response.status_code >= 400:
|
|
@@ -809,6 +865,9 @@ class DailyBotClient:
|
|
|
809
865
|
include_questions: bool = False,
|
|
810
866
|
include_archived: bool = False,
|
|
811
867
|
owner: str | None = None,
|
|
868
|
+
filter_scope: str | None = None,
|
|
869
|
+
order: str | None = None,
|
|
870
|
+
is_ascend: bool = False,
|
|
812
871
|
search: str | None = None,
|
|
813
872
|
start_date: str | None = None,
|
|
814
873
|
end_date: str | None = None,
|
|
@@ -821,7 +880,8 @@ class DailyBotClient:
|
|
|
821
880
|
"""GET /v1/forms/ — optionally expand questions, search, and page.
|
|
822
881
|
|
|
823
882
|
The default response is org-scoped (every form the caller can see on the
|
|
824
|
-
webapp list view). Pass ``owner="me"`` to narrow to the caller's own forms
|
|
883
|
+
webapp list view). Pass ``owner="me"`` to narrow to the caller's own forms,
|
|
884
|
+
or ``filter_scope`` to apply a server-side scope filter.
|
|
825
885
|
|
|
826
886
|
When ``meta`` is given it is populated with ``count`` / ``next`` for a
|
|
827
887
|
pagination footer.
|
|
@@ -833,6 +893,12 @@ class DailyBotClient:
|
|
|
833
893
|
params["include_archived"] = "true"
|
|
834
894
|
if owner:
|
|
835
895
|
params["owner"] = owner
|
|
896
|
+
if filter_scope:
|
|
897
|
+
params["filter"] = filter_scope
|
|
898
|
+
if order:
|
|
899
|
+
params["order"] = order
|
|
900
|
+
if is_ascend:
|
|
901
|
+
params["is_ascend"] = "true"
|
|
836
902
|
_merge_list_query(params, search=search, start_date=start_date, end_date=end_date)
|
|
837
903
|
result: PaginatedResult = self._paginated_get(
|
|
838
904
|
f"{self.api_url}/v1/forms/",
|
|
@@ -858,11 +924,25 @@ class DailyBotClient:
|
|
|
858
924
|
self,
|
|
859
925
|
form_uuid: str,
|
|
860
926
|
content: dict[str, Any],
|
|
927
|
+
*,
|
|
928
|
+
automation: bool = False,
|
|
929
|
+
anonymous: bool = False,
|
|
930
|
+
guest_user: dict[str, str] | None = None,
|
|
931
|
+
submission_source: str | None = None,
|
|
861
932
|
) -> dict[str, Any]:
|
|
862
933
|
"""POST /v1/forms/<form_uuid>/responses/"""
|
|
934
|
+
payload: dict[str, Any] = {"content": content}
|
|
935
|
+
if automation:
|
|
936
|
+
payload["automation"] = True
|
|
937
|
+
if anonymous:
|
|
938
|
+
payload["anonymous"] = True
|
|
939
|
+
if guest_user:
|
|
940
|
+
payload["guest_user"] = guest_user
|
|
941
|
+
if submission_source:
|
|
942
|
+
payload["submission_source"] = submission_source
|
|
863
943
|
response: httpx.Response = httpx.post(
|
|
864
944
|
f"{self.api_url}/v1/forms/{form_uuid}/responses/",
|
|
865
|
-
json=
|
|
945
|
+
json=payload,
|
|
866
946
|
headers=self._headers(),
|
|
867
947
|
timeout=self.timeout,
|
|
868
948
|
)
|
|
@@ -875,6 +955,11 @@ class DailyBotClient:
|
|
|
875
955
|
state: str | None = None,
|
|
876
956
|
all_responses: bool = False,
|
|
877
957
|
user: str | None = None,
|
|
958
|
+
submission_sources: str | None = None,
|
|
959
|
+
submitter_user_ids: str | None = None,
|
|
960
|
+
flow_status: str | None = None,
|
|
961
|
+
order: str | None = None,
|
|
962
|
+
is_ascend: bool = False,
|
|
878
963
|
date_from: str | None = None,
|
|
879
964
|
date_to: str | None = None,
|
|
880
965
|
search: str | None = None,
|
|
@@ -900,6 +985,16 @@ class DailyBotClient:
|
|
|
900
985
|
params["all"] = "true"
|
|
901
986
|
if user:
|
|
902
987
|
params["user"] = user
|
|
988
|
+
if submission_sources:
|
|
989
|
+
params["submission_sources"] = submission_sources
|
|
990
|
+
if submitter_user_ids:
|
|
991
|
+
params["submitter_user_ids"] = submitter_user_ids
|
|
992
|
+
if flow_status:
|
|
993
|
+
params["flow_status"] = flow_status
|
|
994
|
+
if order:
|
|
995
|
+
params["order"] = order
|
|
996
|
+
if is_ascend:
|
|
997
|
+
params["is_ascend"] = "true"
|
|
903
998
|
if date_from:
|
|
904
999
|
params["date_from"] = date_from
|
|
905
1000
|
if date_to:
|
|
@@ -1381,11 +1476,10 @@ class DailyBotClient:
|
|
|
1381
1476
|
payload["is_milestone"] = True
|
|
1382
1477
|
if co_authors:
|
|
1383
1478
|
payload["co_authors"] = co_authors
|
|
1384
|
-
response: httpx.Response =
|
|
1479
|
+
response: httpx.Response = self._agent_request(
|
|
1480
|
+
"POST",
|
|
1385
1481
|
f"{self.api_url}/v1/agent-reports/",
|
|
1386
1482
|
json=payload,
|
|
1387
|
-
headers=self._agent_headers(),
|
|
1388
|
-
timeout=self.timeout,
|
|
1389
1483
|
)
|
|
1390
1484
|
return self._handle_response(response)
|
|
1391
1485
|
|
|
@@ -1402,21 +1496,19 @@ class DailyBotClient:
|
|
|
1402
1496
|
}
|
|
1403
1497
|
if message:
|
|
1404
1498
|
payload["message"] = message
|
|
1405
|
-
response: httpx.Response =
|
|
1499
|
+
response: httpx.Response = self._agent_request(
|
|
1500
|
+
"POST",
|
|
1406
1501
|
f"{self.api_url}/v1/agent-health/",
|
|
1407
1502
|
json=payload,
|
|
1408
|
-
headers=self._agent_headers(),
|
|
1409
|
-
timeout=self.timeout,
|
|
1410
1503
|
)
|
|
1411
1504
|
return self._handle_response(response)
|
|
1412
1505
|
|
|
1413
1506
|
def get_agent_health(self, agent_name: str) -> dict[str, Any]:
|
|
1414
1507
|
"""GET /v1/agent-health/?agent_name=..."""
|
|
1415
|
-
response: httpx.Response =
|
|
1508
|
+
response: httpx.Response = self._agent_request(
|
|
1509
|
+
"GET",
|
|
1416
1510
|
f"{self.api_url}/v1/agent-health/",
|
|
1417
1511
|
params={"agent_name": agent_name},
|
|
1418
|
-
headers=self._agent_headers(),
|
|
1419
|
-
timeout=self.timeout,
|
|
1420
1512
|
)
|
|
1421
1513
|
return self._handle_response(response)
|
|
1422
1514
|
|
|
@@ -1435,22 +1527,19 @@ class DailyBotClient:
|
|
|
1435
1527
|
}
|
|
1436
1528
|
if webhook_secret:
|
|
1437
1529
|
payload["webhook_secret"] = webhook_secret
|
|
1438
|
-
response: httpx.Response =
|
|
1530
|
+
response: httpx.Response = self._agent_request(
|
|
1531
|
+
"POST",
|
|
1439
1532
|
f"{self.api_url}/v1/agent-webhook/",
|
|
1440
1533
|
json=payload,
|
|
1441
|
-
headers=self._agent_headers(),
|
|
1442
|
-
timeout=self.timeout,
|
|
1443
1534
|
)
|
|
1444
1535
|
return self._handle_response(response)
|
|
1445
1536
|
|
|
1446
1537
|
def unregister_agent_webhook(self, agent_name: str) -> dict[str, Any]:
|
|
1447
1538
|
"""DELETE /v1/agent-webhook/"""
|
|
1448
|
-
response: httpx.Response =
|
|
1539
|
+
response: httpx.Response = self._agent_request(
|
|
1449
1540
|
"DELETE",
|
|
1450
1541
|
f"{self.api_url}/v1/agent-webhook/",
|
|
1451
1542
|
json={"agent_name": agent_name},
|
|
1452
|
-
headers=self._agent_headers(),
|
|
1453
|
-
timeout=self.timeout,
|
|
1454
1543
|
)
|
|
1455
1544
|
return self._handle_response(response)
|
|
1456
1545
|
|
|
@@ -1473,11 +1562,10 @@ class DailyBotClient:
|
|
|
1473
1562
|
}
|
|
1474
1563
|
if metadata:
|
|
1475
1564
|
payload["metadata"] = metadata
|
|
1476
|
-
response: httpx.Response =
|
|
1565
|
+
response: httpx.Response = self._agent_request(
|
|
1566
|
+
"POST",
|
|
1477
1567
|
f"{self.api_url}/v1/agent-email/send/",
|
|
1478
1568
|
json=payload,
|
|
1479
|
-
headers=self._agent_headers(),
|
|
1480
|
-
timeout=self.timeout,
|
|
1481
1569
|
)
|
|
1482
1570
|
return self._handle_response(response)
|
|
1483
1571
|
|
|
@@ -1504,11 +1592,10 @@ class DailyBotClient:
|
|
|
1504
1592
|
parent and — when ``thread_responses`` was sent — one id per reply, all
|
|
1505
1593
|
of which can be fed back in a later call to edit the same message.
|
|
1506
1594
|
"""
|
|
1507
|
-
response: httpx.Response =
|
|
1595
|
+
response: httpx.Response = self._agent_request(
|
|
1596
|
+
"POST",
|
|
1508
1597
|
f"{self.api_url}/v1/send-message/",
|
|
1509
1598
|
json=payload,
|
|
1510
|
-
headers=self._agent_headers(),
|
|
1511
|
-
timeout=self.timeout,
|
|
1512
1599
|
)
|
|
1513
1600
|
return self._handle_response(response)
|
|
1514
1601
|
|
|
@@ -1524,11 +1611,10 @@ class DailyBotClient:
|
|
|
1524
1611
|
the shared agent header logic (``X-API-KEY`` preferred, else the login
|
|
1525
1612
|
Bearer token). Returns ``{"channel": "<slack-conversation-id>"}``.
|
|
1526
1613
|
"""
|
|
1527
|
-
response: httpx.Response =
|
|
1614
|
+
response: httpx.Response = self._agent_request(
|
|
1615
|
+
"POST",
|
|
1528
1616
|
f"{self.api_url}/v1/open-conversation/",
|
|
1529
1617
|
json={"users_uuids": users_uuids},
|
|
1530
|
-
headers=self._agent_headers(),
|
|
1531
|
-
timeout=self.timeout,
|
|
1532
1618
|
)
|
|
1533
1619
|
return self._handle_response(response)
|
|
1534
1620
|
|
|
@@ -1559,11 +1645,10 @@ class DailyBotClient:
|
|
|
1559
1645
|
payload["sender_type"] = sender_type
|
|
1560
1646
|
if sender_name:
|
|
1561
1647
|
payload["sender_name"] = sender_name
|
|
1562
|
-
response: httpx.Response =
|
|
1648
|
+
response: httpx.Response = self._agent_request(
|
|
1649
|
+
"POST",
|
|
1563
1650
|
f"{self.api_url}/v1/agent-messages/",
|
|
1564
1651
|
json=payload,
|
|
1565
|
-
headers=self._agent_headers(),
|
|
1566
|
-
timeout=self.timeout,
|
|
1567
1652
|
)
|
|
1568
1653
|
return self._handle_response(response)
|
|
1569
1654
|
|
|
@@ -1576,11 +1661,10 @@ class DailyBotClient:
|
|
|
1576
1661
|
params: dict[str, str] = {"agent_name": agent_name}
|
|
1577
1662
|
if delivered is not None:
|
|
1578
1663
|
params["delivered"] = "true" if delivered else "false"
|
|
1579
|
-
response: httpx.Response =
|
|
1664
|
+
response: httpx.Response = self._agent_request(
|
|
1665
|
+
"GET",
|
|
1580
1666
|
f"{self.api_url}/v1/agent-messages/",
|
|
1581
1667
|
params=params,
|
|
1582
|
-
headers=self._agent_headers(),
|
|
1583
|
-
timeout=self.timeout,
|
|
1584
1668
|
)
|
|
1585
1669
|
if response.status_code >= 400:
|
|
1586
1670
|
self._handle_response(response)
|
|
@@ -1596,11 +1680,10 @@ class DailyBotClient:
|
|
|
1596
1680
|
message_ids: list[str],
|
|
1597
1681
|
) -> dict[str, Any]:
|
|
1598
1682
|
"""PATCH /v1/agent-messages/read/"""
|
|
1599
|
-
response: httpx.Response =
|
|
1683
|
+
response: httpx.Response = self._agent_request(
|
|
1684
|
+
"PATCH",
|
|
1600
1685
|
f"{self.api_url}/v1/agent-messages/read/",
|
|
1601
1686
|
json={"message_ids": message_ids},
|
|
1602
|
-
headers=self._agent_headers(),
|
|
1603
|
-
timeout=self.timeout,
|
|
1604
1687
|
)
|
|
1605
1688
|
return self._handle_response(response)
|
|
1606
1689
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Form commands for the user-scoped public API."""
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
from collections.abc import Callable
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
@@ -59,6 +60,16 @@ from dailybot_cli.display import (
|
|
|
59
60
|
# The server reuses `invalid_workflow_state` for a malformed `--state
|
|
60
61
|
# "Label:#color"` during authoring. On this listing it means the form has no
|
|
61
62
|
# workflow at all, so the shared message would send the user down the wrong path.
|
|
63
|
+
MAX_SUBMISSION_SOURCE_LENGTH: int = 512
|
|
64
|
+
_EMAIL_RE: re.Pattern[str] = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
|
65
|
+
|
|
66
|
+
FORM_LIST_FILTERS: tuple[str, ...] = ("all", "me", "public", "approval", "workflow", "archived")
|
|
67
|
+
FORM_LIST_ORDERS: tuple[str, ...] = ("alphabetical", "recent", "total")
|
|
68
|
+
RESPONSE_ORDERS: tuple[str, ...] = ("recent", "oldest")
|
|
69
|
+
RESPONSE_SOURCES: tuple[str, ...] = ("member", "anonymous", "automation", "public")
|
|
70
|
+
RESPONSE_FLOW_STATUSES: tuple[str, ...] = ("pending", "approved", "denied")
|
|
71
|
+
MAX_SUBMITTER_IDS: int = 50
|
|
72
|
+
|
|
62
73
|
_RESPONSES_ERROR_OVERRIDES: dict[str, str] = {
|
|
63
74
|
"invalid_workflow_state": (
|
|
64
75
|
"This form has no workflow, so --state doesn't apply. Drop the flag, or run "
|
|
@@ -193,11 +204,42 @@ def form() -> None:
|
|
|
193
204
|
is_flag=True,
|
|
194
205
|
help="Only forms you own (default lists every form you can see in the org).",
|
|
195
206
|
)
|
|
207
|
+
@click.option(
|
|
208
|
+
"--filter",
|
|
209
|
+
"filter_scope",
|
|
210
|
+
type=click.Choice(FORM_LIST_FILTERS, case_sensitive=False),
|
|
211
|
+
default=None,
|
|
212
|
+
help="Scope filter: all, me, public, approval, workflow, archived.",
|
|
213
|
+
)
|
|
214
|
+
@click.option(
|
|
215
|
+
"--order",
|
|
216
|
+
type=click.Choice(FORM_LIST_ORDERS, case_sensitive=False),
|
|
217
|
+
default=None,
|
|
218
|
+
help="Sort order: alphabetical, recent, or total responses.",
|
|
219
|
+
)
|
|
220
|
+
@click.option(
|
|
221
|
+
"--ascending",
|
|
222
|
+
"--asc",
|
|
223
|
+
"is_ascend",
|
|
224
|
+
is_flag=True,
|
|
225
|
+
default=False,
|
|
226
|
+
help="Sort ascending (default: descending).",
|
|
227
|
+
)
|
|
228
|
+
@click.option(
|
|
229
|
+
"--include-questions",
|
|
230
|
+
is_flag=True,
|
|
231
|
+
default=False,
|
|
232
|
+
help="Include question definitions in each form.",
|
|
233
|
+
)
|
|
196
234
|
@query_options
|
|
197
235
|
@click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
|
|
198
236
|
def form_list(
|
|
199
237
|
include_archived: bool,
|
|
200
238
|
mine: bool,
|
|
239
|
+
filter_scope: str | None,
|
|
240
|
+
order: str | None,
|
|
241
|
+
is_ascend: bool,
|
|
242
|
+
include_questions: bool,
|
|
201
243
|
json_mode: bool,
|
|
202
244
|
page: int | None,
|
|
203
245
|
page_size: int | None,
|
|
@@ -215,16 +257,19 @@ def form_list(
|
|
|
215
257
|
\b
|
|
216
258
|
Acts as you. You can only see and act on what you could in the webapp:
|
|
217
259
|
by default this is every form in your org you have list-view access to.
|
|
218
|
-
Pass --mine to narrow to only the forms you own.
|
|
219
|
-
Archived forms are hidden unless you pass --include-archived.
|
|
260
|
+
Pass --mine to narrow to only the forms you own, or --filter to scope.
|
|
261
|
+
Archived forms are hidden unless you pass --include-archived or --filter archived.
|
|
220
262
|
|
|
221
263
|
\b
|
|
222
264
|
Examples:
|
|
223
265
|
dailybot form list
|
|
224
266
|
dailybot form list --mine
|
|
267
|
+
dailybot form list --filter workflow --order alphabetical --asc
|
|
268
|
+
dailybot form list --filter approval
|
|
269
|
+
dailybot form list --filter public --order total
|
|
225
270
|
dailybot form list --search retro --since 2026-07-01
|
|
271
|
+
dailybot form list --include-questions --json
|
|
226
272
|
dailybot form list --all
|
|
227
|
-
dailybot form list --json
|
|
228
273
|
"""
|
|
229
274
|
enforce_plan_access("form_list", json_mode=json_mode)
|
|
230
275
|
try:
|
|
@@ -248,7 +293,11 @@ def form_list(
|
|
|
248
293
|
client,
|
|
249
294
|
json_mode=json_mode,
|
|
250
295
|
include_archived=include_archived,
|
|
296
|
+
include_questions=include_questions,
|
|
251
297
|
owner="me" if mine else None,
|
|
298
|
+
filter_scope=filter_scope,
|
|
299
|
+
order=order,
|
|
300
|
+
is_ascend=is_ascend,
|
|
252
301
|
spec=spec,
|
|
253
302
|
)
|
|
254
303
|
|
|
@@ -290,11 +339,50 @@ def form_get(form_uuid: str, json_mode: bool) -> None:
|
|
|
290
339
|
)
|
|
291
340
|
@click.option("--yes", "-y", "assume_yes", is_flag=True, help="Skip the confirmation prompt.")
|
|
292
341
|
@click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
|
|
342
|
+
@click.option(
|
|
343
|
+
"--automation",
|
|
344
|
+
is_flag=True,
|
|
345
|
+
default=False,
|
|
346
|
+
help=(
|
|
347
|
+
"Submit as an automation. The response will appear in channel "
|
|
348
|
+
"notifications without any submitter name — useful when "
|
|
349
|
+
"forwarding third-party form submissions via integrations."
|
|
350
|
+
),
|
|
351
|
+
)
|
|
352
|
+
@click.option(
|
|
353
|
+
"--anonymous",
|
|
354
|
+
is_flag=True,
|
|
355
|
+
default=False,
|
|
356
|
+
help=(
|
|
357
|
+
"Submit anonymously. The response will appear with a random "
|
|
358
|
+
"generated name instead of your real name."
|
|
359
|
+
),
|
|
360
|
+
)
|
|
361
|
+
@click.option(
|
|
362
|
+
"--guest-name",
|
|
363
|
+
default=None,
|
|
364
|
+
help="Guest submitter name (used with --automation for third-party submissions).",
|
|
365
|
+
)
|
|
366
|
+
@click.option(
|
|
367
|
+
"--guest-email",
|
|
368
|
+
default=None,
|
|
369
|
+
help="Guest submitter email (used with --automation for third-party submissions).",
|
|
370
|
+
)
|
|
371
|
+
@click.option(
|
|
372
|
+
"--source",
|
|
373
|
+
default=None,
|
|
374
|
+
help="Provenance label for this submission (max 512 chars, e.g. 'workflow:release-pipeline').",
|
|
375
|
+
)
|
|
293
376
|
def form_submit(
|
|
294
377
|
form_uuid: str,
|
|
295
378
|
content: str | None,
|
|
296
379
|
assume_yes: bool,
|
|
297
380
|
json_mode: bool,
|
|
381
|
+
automation: bool,
|
|
382
|
+
anonymous: bool,
|
|
383
|
+
guest_name: str | None,
|
|
384
|
+
guest_email: str | None,
|
|
385
|
+
source: str | None,
|
|
298
386
|
) -> None:
|
|
299
387
|
"""Submit a form response.
|
|
300
388
|
|
|
@@ -309,7 +397,29 @@ def form_submit(
|
|
|
309
397
|
dailybot form submit <form_uuid>
|
|
310
398
|
dailybot form submit <form_uuid> --content '{"<question_uuid>":"Yes"}'
|
|
311
399
|
dailybot form submit <form_uuid> --content '{"<uuid>":"Answer"}' --yes
|
|
400
|
+
dailybot form submit <form_uuid> --content '{"<uuid>":"A"}' --automation
|
|
401
|
+
dailybot form submit <form_uuid> --content '{"<uuid>":"A"}' --anonymous
|
|
402
|
+
dailybot form submit <form_uuid> --content '{"<uuid>":"A"}' --automation \\
|
|
403
|
+
--guest-name "Release Bot" --guest-email "bot@example.com" \\
|
|
404
|
+
--source "workflow:production-deploy"
|
|
312
405
|
"""
|
|
406
|
+
if guest_email and not _EMAIL_RE.match(guest_email):
|
|
407
|
+
print_error(f"Invalid email format: {guest_email}")
|
|
408
|
+
raise SystemExit(1)
|
|
409
|
+
if source and len(source) > MAX_SUBMISSION_SOURCE_LENGTH:
|
|
410
|
+
print_error(
|
|
411
|
+
f"--source is too long ({len(source)} chars, max {MAX_SUBMISSION_SOURCE_LENGTH})."
|
|
412
|
+
)
|
|
413
|
+
raise SystemExit(1)
|
|
414
|
+
|
|
415
|
+
guest_user: dict[str, str] | None = None
|
|
416
|
+
if guest_name or guest_email:
|
|
417
|
+
guest_user = {}
|
|
418
|
+
if guest_name:
|
|
419
|
+
guest_user["full_name"] = guest_name
|
|
420
|
+
if guest_email:
|
|
421
|
+
guest_user["email"] = guest_email
|
|
422
|
+
|
|
313
423
|
client = require_auth()
|
|
314
424
|
content_map = resolve_form_content(client, form_uuid, content)
|
|
315
425
|
execute_form_submit(
|
|
@@ -318,6 +428,10 @@ def form_submit(
|
|
|
318
428
|
content_map,
|
|
319
429
|
assume_yes=assume_yes,
|
|
320
430
|
json_mode=json_mode,
|
|
431
|
+
automation=automation,
|
|
432
|
+
anonymous=anonymous,
|
|
433
|
+
guest_user=guest_user,
|
|
434
|
+
submission_source=source,
|
|
321
435
|
)
|
|
322
436
|
|
|
323
437
|
|
|
@@ -332,15 +446,48 @@ def form_submit(
|
|
|
332
446
|
"--all",
|
|
333
447
|
"all_responses",
|
|
334
448
|
is_flag=True,
|
|
335
|
-
help="List everyone's responses (
|
|
449
|
+
help="List everyone's responses (requires VIEW_REPORTS permission).",
|
|
336
450
|
)
|
|
337
451
|
@click.option("--user", default=None, help="Filter to one user's responses (admin/owner only).")
|
|
452
|
+
@click.option(
|
|
453
|
+
"--source",
|
|
454
|
+
"submission_sources",
|
|
455
|
+
default=None,
|
|
456
|
+
help="Filter by origin (CSV: member, anonymous, automation, public).",
|
|
457
|
+
)
|
|
458
|
+
@click.option(
|
|
459
|
+
"--submitter",
|
|
460
|
+
"submitter_ids",
|
|
461
|
+
default=None,
|
|
462
|
+
help="Filter by submitter UUIDs (CSV, max 50).",
|
|
463
|
+
)
|
|
464
|
+
@click.option(
|
|
465
|
+
"--flow-status",
|
|
466
|
+
"flow_status",
|
|
467
|
+
type=click.Choice(RESPONSE_FLOW_STATUSES, case_sensitive=False),
|
|
468
|
+
default=None,
|
|
469
|
+
help="Approval flow status: pending, approved, denied.",
|
|
470
|
+
)
|
|
471
|
+
@click.option(
|
|
472
|
+
"--order",
|
|
473
|
+
type=click.Choice(RESPONSE_ORDERS, case_sensitive=False),
|
|
474
|
+
default=None,
|
|
475
|
+
help="Sort order: recent or oldest.",
|
|
476
|
+
)
|
|
477
|
+
@click.option(
|
|
478
|
+
"--ascending",
|
|
479
|
+
"--asc",
|
|
480
|
+
"is_ascend",
|
|
481
|
+
is_flag=True,
|
|
482
|
+
default=False,
|
|
483
|
+
help="Sort ascending (alias for --order oldest).",
|
|
484
|
+
)
|
|
338
485
|
@click.option(
|
|
339
486
|
"--from", "date_from", default=None, help="Responses on/after this date (YYYY-MM-DD)."
|
|
340
487
|
)
|
|
341
488
|
@click.option("--to", "date_to", default=None, help="Responses on/before this date (YYYY-MM-DD).")
|
|
342
489
|
@click.option(
|
|
343
|
-
"--search", "-s", "search", default=None, help="
|
|
490
|
+
"--search", "-s", "search", default=None, help="Search content and submitter (max 256 chars)."
|
|
344
491
|
)
|
|
345
492
|
@click.option(
|
|
346
493
|
"--latest",
|
|
@@ -354,6 +501,11 @@ def form_responses(
|
|
|
354
501
|
state: str | None,
|
|
355
502
|
all_responses: bool,
|
|
356
503
|
user: str | None,
|
|
504
|
+
submission_sources: str | None,
|
|
505
|
+
submitter_ids: str | None,
|
|
506
|
+
flow_status: str | None,
|
|
507
|
+
order: str | None,
|
|
508
|
+
is_ascend: bool,
|
|
357
509
|
date_from: str | None,
|
|
358
510
|
date_to: str | None,
|
|
359
511
|
search: str | None,
|
|
@@ -367,17 +519,45 @@ def form_responses(
|
|
|
367
519
|
|
|
368
520
|
\b
|
|
369
521
|
Acts as you. By default the server returns only responses you authored.
|
|
370
|
-
--all / --user surface others' responses and
|
|
371
|
-
(server-enforced — a member receives 403).
|
|
522
|
+
--all / --user surface others' responses and require VIEW_REPORTS
|
|
523
|
+
permission (server-enforced — a member receives 403).
|
|
524
|
+
|
|
525
|
+
\b
|
|
526
|
+
--source filters by origin: member, anonymous, automation, public.
|
|
527
|
+
Multiple values are comma-separated (OR semantics).
|
|
528
|
+
--submitter filters by specific user UUIDs (CSV, max 50, OR semantics).
|
|
529
|
+
--flow-status filters approval status: pending, approved, denied.
|
|
372
530
|
|
|
373
531
|
\b
|
|
374
532
|
Examples:
|
|
375
533
|
dailybot form responses <form_uuid>
|
|
376
534
|
dailybot form responses <form_uuid> --state qa --json
|
|
377
535
|
dailybot form responses <form_uuid> --all --from 2026-01-01 --to 2026-06-30
|
|
536
|
+
dailybot form responses <form_uuid> --all --source automation
|
|
537
|
+
dailybot form responses <form_uuid> --all --source "member,automation"
|
|
538
|
+
dailybot form responses <form_uuid> --all --flow-status pending
|
|
539
|
+
dailybot form responses <form_uuid> --all --search "deploy" --order oldest
|
|
378
540
|
dailybot form responses <form_uuid> --user <user_uuid> --json
|
|
379
541
|
"""
|
|
380
542
|
validate_user_filter(user)
|
|
543
|
+
if submission_sources:
|
|
544
|
+
parts: list[str] = [s.strip() for s in submission_sources.split(",")]
|
|
545
|
+
invalid: list[str] = [s for s in parts if s not in RESPONSE_SOURCES]
|
|
546
|
+
if invalid:
|
|
547
|
+
print_error(
|
|
548
|
+
f"Invalid source(s): {', '.join(invalid)}. Allowed: {', '.join(RESPONSE_SOURCES)}."
|
|
549
|
+
)
|
|
550
|
+
raise SystemExit(1)
|
|
551
|
+
if submitter_ids:
|
|
552
|
+
ids: list[str] = [s.strip() for s in submitter_ids.split(",")]
|
|
553
|
+
if len(ids) > MAX_SUBMITTER_IDS:
|
|
554
|
+
print_error(f"Too many submitter UUIDs ({len(ids)}, max {MAX_SUBMITTER_IDS}).")
|
|
555
|
+
raise SystemExit(1)
|
|
556
|
+
|
|
557
|
+
effective_order: str | None = order
|
|
558
|
+
if is_ascend and not order:
|
|
559
|
+
effective_order = "oldest"
|
|
560
|
+
|
|
381
561
|
client = require_auth()
|
|
382
562
|
truncated_search: str | None = search[:256] if search is not None else None
|
|
383
563
|
meta: dict[str, Any] = {}
|
|
@@ -388,6 +568,11 @@ def form_responses(
|
|
|
388
568
|
state=state,
|
|
389
569
|
all_responses=all_responses,
|
|
390
570
|
user=user,
|
|
571
|
+
submission_sources=submission_sources,
|
|
572
|
+
submitter_user_ids=submitter_ids,
|
|
573
|
+
flow_status=flow_status,
|
|
574
|
+
order=effective_order,
|
|
575
|
+
is_ascend=is_ascend,
|
|
391
576
|
date_from=date_from,
|
|
392
577
|
date_to=date_to,
|
|
393
578
|
search=truncated_search,
|
|
@@ -234,6 +234,18 @@ ERROR_CODE_MESSAGES: dict[str, str] = {
|
|
|
234
234
|
"search_query_too_long": (
|
|
235
235
|
"Search term is too long (maximum 256 characters). Shorten it and try again."
|
|
236
236
|
),
|
|
237
|
+
"guest_user_required": (
|
|
238
|
+
"This form requires guest submitter info. "
|
|
239
|
+
"Add --guest-name and --guest-email to your command."
|
|
240
|
+
),
|
|
241
|
+
"invalid_filter": "Unknown --filter value. Use: all, me, public, approval, workflow, archived.",
|
|
242
|
+
"invalid_order": "Unknown --order value. Use: alphabetical, recent, total (forms) or recent, oldest (responses).",
|
|
243
|
+
"invalid_submission_sources": (
|
|
244
|
+
"Unknown --source value. Use: member, anonymous, automation, public (comma-separated)."
|
|
245
|
+
),
|
|
246
|
+
"invalid_submitter_user_id": "Invalid --submitter UUID format.",
|
|
247
|
+
"too_many_submitter_user_ids": "Too many --submitter UUIDs (max 50).",
|
|
248
|
+
"invalid_flow_status": "Unknown --flow-status value. Use: pending, approved, denied.",
|
|
237
249
|
"invalid_date_range": (
|
|
238
250
|
"Invalid date. Use YYYY-MM-DD, and make sure the start date is on or before the end date."
|
|
239
251
|
),
|
|
@@ -500,19 +500,27 @@ def execute_form_list(
|
|
|
500
500
|
*,
|
|
501
501
|
json_mode: bool = False,
|
|
502
502
|
include_archived: bool = False,
|
|
503
|
+
include_questions: bool = False,
|
|
503
504
|
owner: str | None = None,
|
|
505
|
+
filter_scope: str | None = None,
|
|
506
|
+
order: str | None = None,
|
|
507
|
+
is_ascend: bool = False,
|
|
504
508
|
spec: QuerySpec | None = None,
|
|
505
509
|
) -> list[dict[str, Any]] | None:
|
|
506
510
|
"""Fetch and display forms visible to the user (with question counts)."""
|
|
507
511
|
query: QuerySpec = spec or QuerySpec()
|
|
508
512
|
fetch_all: bool = resolve_fetch_all(query)
|
|
509
513
|
meta: dict[str, Any] = {}
|
|
514
|
+
always_include_questions: bool = include_questions or not json_mode
|
|
510
515
|
try:
|
|
511
516
|
with console.status("Fetching forms..."):
|
|
512
517
|
forms: list[dict[str, Any]] = client.list_forms(
|
|
513
|
-
include_questions=
|
|
518
|
+
include_questions=always_include_questions,
|
|
514
519
|
include_archived=include_archived,
|
|
515
520
|
owner=owner,
|
|
521
|
+
filter_scope=filter_scope,
|
|
522
|
+
order=order,
|
|
523
|
+
is_ascend=is_ascend,
|
|
516
524
|
search=query.params.get("search"),
|
|
517
525
|
start_date=query.params.get("start_date"),
|
|
518
526
|
end_date=query.params.get("end_date"),
|
|
@@ -542,6 +550,10 @@ def execute_form_submit(
|
|
|
542
550
|
form_name: str | None = None,
|
|
543
551
|
assume_yes: bool = False,
|
|
544
552
|
json_mode: bool = False,
|
|
553
|
+
automation: bool = False,
|
|
554
|
+
anonymous: bool = False,
|
|
555
|
+
guest_user: dict[str, str] | None = None,
|
|
556
|
+
submission_source: str | None = None,
|
|
545
557
|
) -> None:
|
|
546
558
|
"""Submit a form response."""
|
|
547
559
|
resolved_name: str = form_name or form_uuid
|
|
@@ -556,8 +568,19 @@ def execute_form_submit(
|
|
|
556
568
|
summary_lines: list[str] = [
|
|
557
569
|
f"Form: {resolved_name}",
|
|
558
570
|
f"Form UUID: {form_uuid}",
|
|
559
|
-
"Answers:",
|
|
560
571
|
]
|
|
572
|
+
if automation:
|
|
573
|
+
summary_lines.append("Mode: automation (no submitter shown in channel)")
|
|
574
|
+
if anonymous:
|
|
575
|
+
summary_lines.append("Mode: anonymous (random name shown in channel)")
|
|
576
|
+
if guest_user:
|
|
577
|
+
guest_desc: str = guest_user.get("full_name", "")
|
|
578
|
+
if guest_user.get("email"):
|
|
579
|
+
guest_desc += f" <{guest_user['email']}>" if guest_desc else guest_user["email"]
|
|
580
|
+
summary_lines.append(f"Guest: {guest_desc}")
|
|
581
|
+
if submission_source:
|
|
582
|
+
summary_lines.append(f"Source: {submission_source}")
|
|
583
|
+
summary_lines.append("Answers:")
|
|
561
584
|
for question_uuid, answer in content_map.items():
|
|
562
585
|
summary_lines.append(f" {question_uuid}: {answer}")
|
|
563
586
|
|
|
@@ -568,6 +591,10 @@ def execute_form_submit(
|
|
|
568
591
|
result: dict[str, Any] = client.submit_form_response(
|
|
569
592
|
form_uuid=form_uuid,
|
|
570
593
|
content=content_map,
|
|
594
|
+
automation=automation,
|
|
595
|
+
anonymous=anonymous,
|
|
596
|
+
guest_user=guest_user,
|
|
597
|
+
submission_source=submission_source,
|
|
571
598
|
)
|
|
572
599
|
except APIError as exc:
|
|
573
600
|
exit_for_api_error(exc, json_mode)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dailybot-cli
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.5.1
|
|
4
4
|
Summary: Dailybot CLI - The command-line bridge between humans and agents. Progress reports, observability, and workflow automation for modern teams across Slack, Google Chat, Discord, MS Teams, and more.
|
|
5
5
|
Author-email: Dailybot <support@dailybot.com>
|
|
6
6
|
License: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dailybot-cli"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.5.1"
|
|
8
8
|
description = "Dailybot CLI - The command-line bridge between humans and agents. Progress reports, observability, and workflow automation for modern teams across Slack, Google Chat, Discord, MS Teams, and more."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|