workweek 0.4.2__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.
- workweek/__init__.py +6 -0
- workweek/agents.py +24 -0
- workweek/analysis.py +16 -0
- workweek/apps.py +41 -0
- workweek/chat.py +182 -0
- workweek/client.py +108 -0
- workweek/data.py +69 -0
- workweek/execution.py +129 -0
- workweek/knowledge.py +23 -0
- workweek/places.py +142 -0
- workweek/teams.py +20 -0
- workweek-0.4.2.dist-info/METADATA +127 -0
- workweek-0.4.2.dist-info/RECORD +16 -0
- workweek-0.4.2.dist-info/WHEEL +5 -0
- workweek-0.4.2.dist-info/licenses/LICENSE +190 -0
- workweek-0.4.2.dist-info/top_level.txt +1 -0
workweek/__init__.py
ADDED
workweek/agents.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Agents module — manage saved agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from workweek.client import WorkWeekClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AgentsModule:
|
|
11
|
+
def __init__(self, client: WorkWeekClient):
|
|
12
|
+
self._client = client
|
|
13
|
+
|
|
14
|
+
def list(self, limit: int = 50, offset: int = 0) -> dict:
|
|
15
|
+
"""List saved agents."""
|
|
16
|
+
return self._client.get("/api/v1/agents", params={"limit": limit, "offset": offset})
|
|
17
|
+
|
|
18
|
+
def get(self, agent_id: str) -> dict:
|
|
19
|
+
"""Get agent details."""
|
|
20
|
+
return self._client.get(f"/api/v1/agents/{agent_id}")
|
|
21
|
+
|
|
22
|
+
def create(self, data: dict) -> dict:
|
|
23
|
+
"""Create a new agent."""
|
|
24
|
+
return self._client.post("/api/v1/agents", json=data)
|
workweek/analysis.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Analysis module — BACI and statistical analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from workweek.client import WorkWeekClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AnalysisModule:
|
|
11
|
+
def __init__(self, client: WorkWeekClient):
|
|
12
|
+
self._client = client
|
|
13
|
+
|
|
14
|
+
def run_baci(self, slug: str, params: dict) -> dict:
|
|
15
|
+
"""Run BACI (Before-After-Control-Impact) analysis for an app."""
|
|
16
|
+
return self._client.post(f"/api/v1/app-api/{slug}/baci", json=params)
|
workweek/apps.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Apps module — tenant app config and page data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import TYPE_CHECKING, Optional
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from workweek.client import WorkWeekClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AppsModule:
|
|
11
|
+
def __init__(self, client: WorkWeekClient):
|
|
12
|
+
self._client = client
|
|
13
|
+
|
|
14
|
+
def get_config(self, slug: str) -> dict:
|
|
15
|
+
"""Get full app config (pages, charts, branding) for a tenant app."""
|
|
16
|
+
return self._client.get(f"/api/v1/app-api/{slug}/config")
|
|
17
|
+
|
|
18
|
+
def get_page_data(self, slug: str, page: str, filters: Optional[dict] = None) -> dict:
|
|
19
|
+
"""Get chart data for a specific page, optionally with filters."""
|
|
20
|
+
params = {}
|
|
21
|
+
if filters:
|
|
22
|
+
params.update(filters)
|
|
23
|
+
return self._client.get(f"/api/v1/app-api/{slug}/page/{page}/data", params=params)
|
|
24
|
+
|
|
25
|
+
def get_filter_options(self, slug: str, page: str, filter_key: str) -> dict:
|
|
26
|
+
"""Get available filter options for a page filter."""
|
|
27
|
+
return self._client.get(
|
|
28
|
+
f"/api/v1/app-api/{slug}/page/{page}/filter-options",
|
|
29
|
+
params={"filter_key": filter_key},
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def update_frontend_config(self, slug: str, config: dict) -> dict:
|
|
33
|
+
"""Update the frontend config for an app (pages, charts, branding)."""
|
|
34
|
+
return self._client.patch(
|
|
35
|
+
f"/api/v1/app-api/{slug}/frontend-config",
|
|
36
|
+
json=config,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def list_apps(self) -> dict:
|
|
40
|
+
"""List all apps for the authenticated organization."""
|
|
41
|
+
return self._client.get("/api/v1/apps")
|
workweek/chat.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Chat module — conversational AI with SSE streaming.
|
|
2
|
+
|
|
3
|
+
Wraps the chat-service /api/v1/chat/message Server-Sent Events stream into
|
|
4
|
+
a Python iterator of typed events. Supports tool-calling progress
|
|
5
|
+
(tool_start/tool_end) and incremental token output.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
import json
|
|
10
|
+
from typing import TYPE_CHECKING, Iterator, Optional
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from workweek.client import WorkWeekClient
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChatEvent:
|
|
17
|
+
"""A single SSE event from the chat-service /message stream.
|
|
18
|
+
|
|
19
|
+
Event types:
|
|
20
|
+
session — initial session id confirmation
|
|
21
|
+
tool_start — an MCP tool invocation began
|
|
22
|
+
tool_end — an MCP tool invocation finished (with duration_ms)
|
|
23
|
+
token — a partial assistant message token
|
|
24
|
+
done — stream finished (final event)
|
|
25
|
+
error — an error occurred mid-stream
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, event_type: str, data: dict):
|
|
29
|
+
self.type = event_type
|
|
30
|
+
self.data = data
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def content(self) -> str:
|
|
34
|
+
"""For 'token' events, the partial text. Empty string otherwise."""
|
|
35
|
+
return self.data.get("content", "") if self.type == "token" else ""
|
|
36
|
+
|
|
37
|
+
def __repr__(self) -> str:
|
|
38
|
+
return f"ChatEvent(type={self.type!r}, data={self.data!r})"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ChatModule:
|
|
42
|
+
def __init__(self, client: WorkWeekClient):
|
|
43
|
+
self._client = client
|
|
44
|
+
|
|
45
|
+
def stream(
|
|
46
|
+
self,
|
|
47
|
+
message: str,
|
|
48
|
+
session_id: Optional[str] = None,
|
|
49
|
+
) -> Iterator[ChatEvent]:
|
|
50
|
+
"""Stream a chat message response from chat-service /message.
|
|
51
|
+
|
|
52
|
+
Yields ChatEvent objects until the 'done' event is received.
|
|
53
|
+
|
|
54
|
+
Usage::
|
|
55
|
+
|
|
56
|
+
for event in client.chat.stream("How many food trucks are active?"):
|
|
57
|
+
if event.type == "token":
|
|
58
|
+
print(event.content, end="", flush=True)
|
|
59
|
+
elif event.type == "tool_start":
|
|
60
|
+
print(f"\\n[tool: {event.data.get('tool_name')}]")
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
message: The user message to send.
|
|
64
|
+
session_id: Optional session id for conversation continuity. If
|
|
65
|
+
omitted, the server creates a new session and returns its id
|
|
66
|
+
in the first 'session' event.
|
|
67
|
+
|
|
68
|
+
Yields:
|
|
69
|
+
ChatEvent — one per SSE event from the upstream stream.
|
|
70
|
+
"""
|
|
71
|
+
# Import lazily to avoid circular import on package init.
|
|
72
|
+
from workweek.client import WorkWeekAPIError
|
|
73
|
+
|
|
74
|
+
payload: dict = {"message": message}
|
|
75
|
+
if session_id:
|
|
76
|
+
payload["session_id"] = session_id
|
|
77
|
+
|
|
78
|
+
with self._client._http.stream(
|
|
79
|
+
"POST",
|
|
80
|
+
"/api/v1/chat/message",
|
|
81
|
+
json=payload,
|
|
82
|
+
) as resp:
|
|
83
|
+
if resp.status_code >= 400:
|
|
84
|
+
body = resp.read().decode(errors="replace")
|
|
85
|
+
raise WorkWeekAPIError(resp.status_code, body)
|
|
86
|
+
|
|
87
|
+
for line in resp.iter_lines():
|
|
88
|
+
if not line or not line.startswith("data: "):
|
|
89
|
+
continue
|
|
90
|
+
try:
|
|
91
|
+
data = json.loads(line[6:])
|
|
92
|
+
except json.JSONDecodeError:
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
event_type = data.get("type", "unknown")
|
|
96
|
+
yield ChatEvent(event_type, data)
|
|
97
|
+
if event_type == "done":
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
def send(self, message: str, session_id: Optional[str] = None) -> str:
|
|
101
|
+
"""Convenience: collect all tokens from stream() into a single string.
|
|
102
|
+
|
|
103
|
+
Returns the assembled assistant message text. Use stream() if you need
|
|
104
|
+
intermediate tool events or token-by-token rendering.
|
|
105
|
+
"""
|
|
106
|
+
chunks: list[str] = []
|
|
107
|
+
for event in self.stream(message, session_id=session_id):
|
|
108
|
+
if event.type == "token":
|
|
109
|
+
chunks.append(event.content)
|
|
110
|
+
return "".join(chunks)
|
|
111
|
+
|
|
112
|
+
def with_team(
|
|
113
|
+
self,
|
|
114
|
+
team_id: str,
|
|
115
|
+
message: str,
|
|
116
|
+
session_id: Optional[str] = None,
|
|
117
|
+
) -> Iterator[ChatEvent]:
|
|
118
|
+
"""Stream a message through a declaratively-provisioned team (TD-102 Phase D).
|
|
119
|
+
|
|
120
|
+
Routes through chat-service `/api/v1/chat/message` with the optional
|
|
121
|
+
`team_id` parameter set. Chat-service loads the team, narrows the
|
|
122
|
+
LLM tool list to the team's allowed tools, and prepends the team's
|
|
123
|
+
specialty + backstory + TeamPromptContext additions to the system
|
|
124
|
+
prompt. The result: tool-calling actually works (vs the older
|
|
125
|
+
gateway team-chat path which was a bare LLM stream with no tools).
|
|
126
|
+
|
|
127
|
+
Yields the SAME ChatEvent shape as `client.chat.stream()` —
|
|
128
|
+
session/tool_start/tool_end/token/done. Use the existing chat
|
|
129
|
+
consumer code unchanged; the only difference is the request goes
|
|
130
|
+
through the team's specialty.
|
|
131
|
+
|
|
132
|
+
Usage::
|
|
133
|
+
|
|
134
|
+
session_id = None
|
|
135
|
+
for event in client.chat.with_team(team_id="<uuid>", message="..."):
|
|
136
|
+
if event.type == "session":
|
|
137
|
+
session_id = event.data["session_id"]
|
|
138
|
+
elif event.type == "tool_start":
|
|
139
|
+
print(f"\\n[tool: {event.data.get('tool_name')}]")
|
|
140
|
+
elif event.type == "token":
|
|
141
|
+
print(event.content, end="", flush=True)
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
team_id: UUID of the team (from `POST /api/v1/teams/declare`).
|
|
145
|
+
message: The user message to send.
|
|
146
|
+
session_id: Optional session id for conversation continuity.
|
|
147
|
+
Server-side history is keyed on this id (Redis 7-day TTL).
|
|
148
|
+
If omitted, the server creates a new session and returns its
|
|
149
|
+
id in the first 'session' event.
|
|
150
|
+
|
|
151
|
+
Yields:
|
|
152
|
+
ChatEvent — same shape as `stream()`. Includes tool_start /
|
|
153
|
+
tool_end events when the team's agent uses tools (which it
|
|
154
|
+
should, for any data-shaped query).
|
|
155
|
+
"""
|
|
156
|
+
from workweek.client import WorkWeekAPIError
|
|
157
|
+
import json
|
|
158
|
+
|
|
159
|
+
payload: dict = {"message": message, "team_id": team_id}
|
|
160
|
+
if session_id:
|
|
161
|
+
payload["session_id"] = session_id
|
|
162
|
+
|
|
163
|
+
with self._client._http.stream(
|
|
164
|
+
"POST",
|
|
165
|
+
"/api/v1/chat/message",
|
|
166
|
+
json=payload,
|
|
167
|
+
) as resp:
|
|
168
|
+
if resp.status_code >= 400:
|
|
169
|
+
body = resp.read().decode(errors="replace")
|
|
170
|
+
raise WorkWeekAPIError(resp.status_code, body)
|
|
171
|
+
|
|
172
|
+
for line in resp.iter_lines():
|
|
173
|
+
if not line or not line.startswith("data: "):
|
|
174
|
+
continue
|
|
175
|
+
try:
|
|
176
|
+
data = json.loads(line[6:])
|
|
177
|
+
except json.JSONDecodeError:
|
|
178
|
+
continue
|
|
179
|
+
event_type = data.get("type", "unknown")
|
|
180
|
+
yield ChatEvent(event_type, data)
|
|
181
|
+
if event_type == "done":
|
|
182
|
+
return
|
workweek/client.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Base HTTP client for WorkWeek API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from workweek.data import DataModule
|
|
8
|
+
from workweek.apps import AppsModule
|
|
9
|
+
from workweek.agents import AgentsModule
|
|
10
|
+
from workweek.analysis import AnalysisModule
|
|
11
|
+
from workweek.knowledge import KnowledgeModule
|
|
12
|
+
from workweek.chat import ChatModule
|
|
13
|
+
from workweek.teams import TeamsModule
|
|
14
|
+
from workweek.execution import ExecutionModule
|
|
15
|
+
from workweek.places import PlacesModule
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WorkWeekAPIError(Exception):
|
|
19
|
+
"""Raised when the WorkWeek API returns an error."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, status_code: int, message: str):
|
|
22
|
+
self.status_code = status_code
|
|
23
|
+
self.message = message
|
|
24
|
+
super().__init__(f"HTTP {status_code}: {message}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class WorkWeekClient:
|
|
28
|
+
"""WorkWeek Python SDK client.
|
|
29
|
+
|
|
30
|
+
Usage::
|
|
31
|
+
|
|
32
|
+
from workweek import WorkWeekClient
|
|
33
|
+
|
|
34
|
+
client = WorkWeekClient(
|
|
35
|
+
base_url="https://gw.askvai.com",
|
|
36
|
+
api_key="wk_rpt_...",
|
|
37
|
+
)
|
|
38
|
+
result = client.data.query(
|
|
39
|
+
dataset="sf_food_trucks_permits",
|
|
40
|
+
sql="SELECT COUNT(*) AS n FROM tbl WHERE status = 'APPROVED'",
|
|
41
|
+
)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, base_url: str, api_key: str, timeout: float = 30.0):
|
|
45
|
+
self._base_url = base_url.rstrip("/")
|
|
46
|
+
self._api_key = api_key
|
|
47
|
+
self._timeout = timeout
|
|
48
|
+
# TD-125: chat streams (chat.stream, chat.with_team) can run well
|
|
49
|
+
# past 30s for multi-tool turns. Give streaming endpoints a
|
|
50
|
+
# longer read timeout; short connect timeout keeps cold-start
|
|
51
|
+
# failure detection quick. Non-stream requests still use the
|
|
52
|
+
# caller's `timeout` value via per-request overrides in modules.
|
|
53
|
+
stream_timeout = httpx.Timeout(
|
|
54
|
+
connect=10.0,
|
|
55
|
+
read=120.0,
|
|
56
|
+
write=timeout,
|
|
57
|
+
pool=timeout,
|
|
58
|
+
)
|
|
59
|
+
self._http = httpx.Client(
|
|
60
|
+
base_url=self._base_url,
|
|
61
|
+
headers={"X-API-Key": api_key},
|
|
62
|
+
timeout=stream_timeout,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Module accessors
|
|
66
|
+
self.data = DataModule(self)
|
|
67
|
+
self.apps = AppsModule(self)
|
|
68
|
+
self.agents = AgentsModule(self)
|
|
69
|
+
self.analysis = AnalysisModule(self)
|
|
70
|
+
self.knowledge = KnowledgeModule(self)
|
|
71
|
+
self.chat = ChatModule(self)
|
|
72
|
+
self.teams = TeamsModule(self)
|
|
73
|
+
self.execution = ExecutionModule(self)
|
|
74
|
+
self.places = PlacesModule(self) # TD-107b — Google Places via Tier 3 BYOK
|
|
75
|
+
|
|
76
|
+
def _request(self, method: str, path: str, **kwargs) -> dict:
|
|
77
|
+
"""Make an authenticated request and return JSON response."""
|
|
78
|
+
resp = self._http.request(method, path, **kwargs)
|
|
79
|
+
if resp.status_code >= 400:
|
|
80
|
+
try:
|
|
81
|
+
detail = resp.json().get("detail", resp.text)
|
|
82
|
+
except Exception:
|
|
83
|
+
detail = resp.text
|
|
84
|
+
raise WorkWeekAPIError(resp.status_code, detail)
|
|
85
|
+
if resp.status_code == 204:
|
|
86
|
+
return {}
|
|
87
|
+
return resp.json()
|
|
88
|
+
|
|
89
|
+
def get(self, path: str, **kwargs) -> dict:
|
|
90
|
+
return self._request("GET", path, **kwargs)
|
|
91
|
+
|
|
92
|
+
def post(self, path: str, **kwargs) -> dict:
|
|
93
|
+
return self._request("POST", path, **kwargs)
|
|
94
|
+
|
|
95
|
+
def patch(self, path: str, **kwargs) -> dict:
|
|
96
|
+
return self._request("PATCH", path, **kwargs)
|
|
97
|
+
|
|
98
|
+
def delete(self, path: str, **kwargs) -> dict:
|
|
99
|
+
return self._request("DELETE", path, **kwargs)
|
|
100
|
+
|
|
101
|
+
def close(self):
|
|
102
|
+
self._http.close()
|
|
103
|
+
|
|
104
|
+
def __enter__(self):
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def __exit__(self, *args):
|
|
108
|
+
self.close()
|
workweek/data.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Data module — query Iceberg datasets via the WorkWeek SDK gateway.
|
|
2
|
+
|
|
3
|
+
All methods hit /api/v1/sdk/* endpoints, which derive org_id from the API key
|
|
4
|
+
and validate that SQL is SELECT-only. Use 'tbl' as the table reference in your
|
|
5
|
+
SQL — every dataset is exposed under that name.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from typing import TYPE_CHECKING, Optional
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from workweek.client import WorkWeekClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DataModule:
|
|
16
|
+
def __init__(self, client: WorkWeekClient):
|
|
17
|
+
self._client = client
|
|
18
|
+
|
|
19
|
+
def query(
|
|
20
|
+
self,
|
|
21
|
+
dataset: str,
|
|
22
|
+
sql: str,
|
|
23
|
+
limit: Optional[int] = None,
|
|
24
|
+
) -> dict:
|
|
25
|
+
"""Execute a SELECT query against an Iceberg dataset.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
dataset: Dataset name (e.g. ``"sf_food_trucks_permits"``).
|
|
29
|
+
sql: DuckDB SELECT query. Use ``tbl`` as the table reference.
|
|
30
|
+
Only SELECT statements are permitted; INSERT/UPDATE/DELETE/
|
|
31
|
+
DROP/ALTER/etc. are rejected with HTTP 422.
|
|
32
|
+
limit: Optional row cap (1-500, defaults to 100 server-side).
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
``{"dataset": str, "row_count": int, "columns": [str], "rows": [dict]}``
|
|
36
|
+
|
|
37
|
+
Example::
|
|
38
|
+
|
|
39
|
+
result = client.data.query(
|
|
40
|
+
dataset="sf_food_trucks_permits",
|
|
41
|
+
sql="SELECT facilitytype, COUNT(*) AS n FROM tbl "
|
|
42
|
+
"WHERE status = 'APPROVED' GROUP BY facilitytype",
|
|
43
|
+
)
|
|
44
|
+
for row in result["rows"]:
|
|
45
|
+
print(row)
|
|
46
|
+
"""
|
|
47
|
+
payload: dict = {"dataset": dataset, "sql": sql}
|
|
48
|
+
if limit is not None:
|
|
49
|
+
payload["limit"] = limit
|
|
50
|
+
return self._client.post("/api/v1/sdk/query", json=payload)
|
|
51
|
+
|
|
52
|
+
def list_datasets(self) -> dict:
|
|
53
|
+
"""List Iceberg datasets accessible to the API key's organization.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
``{"count": int, "datasets": [{"name": str, "row_count": int}]}``
|
|
57
|
+
"""
|
|
58
|
+
return self._client.get("/api/v1/sdk/datasets")
|
|
59
|
+
|
|
60
|
+
def get_schema(self, dataset: str) -> dict:
|
|
61
|
+
"""Get the column schema for a dataset by sampling its first row.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
dataset: Dataset name (e.g. ``"sf_food_trucks_permits"``).
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
``{"dataset": str, "columns": [str], "sample": dict | None}``
|
|
68
|
+
"""
|
|
69
|
+
return self._client.get(f"/api/v1/sdk/datasets/{dataset}/schema")
|
workweek/execution.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Execution module — run queries and track executions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import time
|
|
5
|
+
from typing import TYPE_CHECKING, Optional
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from workweek.client import WorkWeekClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ExecutionModule:
|
|
12
|
+
def __init__(self, client: WorkWeekClient):
|
|
13
|
+
self._client = client
|
|
14
|
+
|
|
15
|
+
def run_query(
|
|
16
|
+
self,
|
|
17
|
+
query: str,
|
|
18
|
+
team_id: Optional[str] = None,
|
|
19
|
+
path_type: Optional[str] = None,
|
|
20
|
+
template_id: Optional[str] = None,
|
|
21
|
+
execution_metadata: Optional[dict] = None,
|
|
22
|
+
custom_instructions: Optional[str] = None,
|
|
23
|
+
) -> dict:
|
|
24
|
+
"""Submit a query for execution.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
query: The research query or task description.
|
|
28
|
+
team_id: Team ID to scope the execution to.
|
|
29
|
+
path_type: Execution path — "crew", "single", "architect", "deep_research", "chat".
|
|
30
|
+
template_id: Research template ID (e.g. "candidate_dossier", "market_analysis").
|
|
31
|
+
Shorthand for execution_metadata={"template_id": ...}.
|
|
32
|
+
execution_metadata: Additional metadata passed to the execution engine.
|
|
33
|
+
For deep_research: template_id, max_steps, coaching_notes, etc.
|
|
34
|
+
custom_instructions: Free-form instructions injected into agent prompts.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
dict with execution_id, status, message.
|
|
38
|
+
"""
|
|
39
|
+
payload: dict = {"query": query}
|
|
40
|
+
if team_id:
|
|
41
|
+
payload["team_id"] = team_id
|
|
42
|
+
if path_type:
|
|
43
|
+
payload["path_type"] = path_type
|
|
44
|
+
if custom_instructions:
|
|
45
|
+
payload["custom_instructions"] = custom_instructions
|
|
46
|
+
|
|
47
|
+
# Build execution_metadata — merge template_id shorthand with explicit metadata
|
|
48
|
+
meta = dict(execution_metadata or {})
|
|
49
|
+
if template_id:
|
|
50
|
+
meta["template_id"] = template_id
|
|
51
|
+
if meta:
|
|
52
|
+
payload["execution_metadata"] = meta
|
|
53
|
+
|
|
54
|
+
return self._client.post("/api/v1/query", json=payload)
|
|
55
|
+
|
|
56
|
+
def run_research(
|
|
57
|
+
self,
|
|
58
|
+
query: str,
|
|
59
|
+
template_id: Optional[str] = None,
|
|
60
|
+
team_id: Optional[str] = None,
|
|
61
|
+
max_steps: Optional[int] = None,
|
|
62
|
+
custom_instructions: Optional[str] = None,
|
|
63
|
+
) -> dict:
|
|
64
|
+
"""Submit a deep research query. Convenience wrapper around run_query().
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
query: The research question or candidate to investigate.
|
|
68
|
+
template_id: Research template — "candidate_dossier", "market_analysis",
|
|
69
|
+
"competitive_landscape", "technical_deep_dive", "financial_analysis",
|
|
70
|
+
"safety_report", "quick_lookup".
|
|
71
|
+
team_id: Team ID to scope the execution to.
|
|
72
|
+
max_steps: Override the template's default step budget (10-150).
|
|
73
|
+
custom_instructions: Additional guidance for the research engine.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
dict with execution_id, status, message.
|
|
77
|
+
"""
|
|
78
|
+
meta: dict = {}
|
|
79
|
+
if template_id:
|
|
80
|
+
meta["template_id"] = template_id
|
|
81
|
+
if max_steps is not None:
|
|
82
|
+
meta["max_steps"] = max_steps
|
|
83
|
+
|
|
84
|
+
return self.run_query(
|
|
85
|
+
query=query,
|
|
86
|
+
team_id=team_id,
|
|
87
|
+
path_type="deep_research",
|
|
88
|
+
execution_metadata=meta or None,
|
|
89
|
+
custom_instructions=custom_instructions,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def wait_for_completion(
|
|
93
|
+
self,
|
|
94
|
+
execution_id: str,
|
|
95
|
+
poll_interval: float = 10.0,
|
|
96
|
+
timeout: float = 1800.0,
|
|
97
|
+
) -> dict:
|
|
98
|
+
"""Poll until execution completes or fails.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
execution_id: The execution to wait for.
|
|
102
|
+
poll_interval: Seconds between status checks (default 10s).
|
|
103
|
+
timeout: Max seconds to wait (default 1800s / 30 min).
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Full execution dict with result.
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
TimeoutError: If execution doesn't complete within timeout.
|
|
110
|
+
"""
|
|
111
|
+
start = time.time()
|
|
112
|
+
while True:
|
|
113
|
+
data = self.get_status(execution_id)
|
|
114
|
+
status = data.get("status", "")
|
|
115
|
+
if status in ("completed", "failed"):
|
|
116
|
+
return data
|
|
117
|
+
if time.time() - start > timeout:
|
|
118
|
+
raise TimeoutError(
|
|
119
|
+
f"Execution {execution_id} still '{status}' after {timeout}s"
|
|
120
|
+
)
|
|
121
|
+
time.sleep(poll_interval)
|
|
122
|
+
|
|
123
|
+
def get_status(self, execution_id: str) -> dict:
|
|
124
|
+
"""Get execution status."""
|
|
125
|
+
return self._client.get(f"/api/v1/executions/{execution_id}")
|
|
126
|
+
|
|
127
|
+
def list_executions(self, limit: int = 20, offset: int = 0) -> dict:
|
|
128
|
+
"""List recent executions."""
|
|
129
|
+
return self._client.get("/api/v1/executions", params={"limit": limit, "offset": offset})
|
workweek/knowledge.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Knowledge module — collections and document search."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from workweek.client import WorkWeekClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class KnowledgeModule:
|
|
11
|
+
def __init__(self, client: WorkWeekClient):
|
|
12
|
+
self._client = client
|
|
13
|
+
|
|
14
|
+
def list_collections(self) -> dict:
|
|
15
|
+
"""List knowledge collections."""
|
|
16
|
+
return self._client.get("/api/v1/knowledge/collections")
|
|
17
|
+
|
|
18
|
+
def search(self, query: str, collection_id: str | None = None) -> dict:
|
|
19
|
+
"""Search knowledge base."""
|
|
20
|
+
params = {"q": query}
|
|
21
|
+
if collection_id:
|
|
22
|
+
params["collection_id"] = collection_id
|
|
23
|
+
return self._client.get("/api/v1/knowledge/search", params=params)
|
workweek/places.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Places module — Google Places proxy via the WorkWeek SDK gateway.
|
|
2
|
+
|
|
3
|
+
Routes through /api/v1/sdk/places/* which resolves the org's Google API key
|
|
4
|
+
from the platform's per-org BYOK entitlements (see the WorkWeek Secrets &
|
|
5
|
+
BYOK Architecture doc — Tier 3 pattern). The SDK caller never sees or
|
|
6
|
+
manages the Google API key; the tenant's platform admin seeds the
|
|
7
|
+
entitlement once per org via the Settings UI or the admin API.
|
|
8
|
+
|
|
9
|
+
Zero-config for SDK callers: if you have a tenant API key and your org has
|
|
10
|
+
a ``google_places`` entitlement, these methods just work. If the entitlement
|
|
11
|
+
is missing, the methods return ``{"found": False, "reason": "not_configured"}``
|
|
12
|
+
instead of raising — frontends can hide the UI section gracefully.
|
|
13
|
+
|
|
14
|
+
Three methods:
|
|
15
|
+
client.places.search(name, lat, lon) → top match by business name
|
|
16
|
+
client.places.details(place_id) → reviews + website + hours
|
|
17
|
+
client.places.streetview(lat, lon) → Google Maps pano deep-link URL
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from workweek.client import WorkWeekClient
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PlacesModule:
|
|
28
|
+
def __init__(self, client: WorkWeekClient):
|
|
29
|
+
self._client = client
|
|
30
|
+
|
|
31
|
+
def search(self, name: str, lat: float, lon: float) -> dict:
|
|
32
|
+
"""Search Google Places for a business by name, biased by lat/lon.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
name: Business name to search for (e.g. ``"Corazon Mexicano"``).
|
|
36
|
+
lat: Latitude to bias search around.
|
|
37
|
+
lon: Longitude to bias search around.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
On match::
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
"found": True,
|
|
44
|
+
"google_name": str,
|
|
45
|
+
"google_rating": float | None,
|
|
46
|
+
"google_total_ratings": int,
|
|
47
|
+
"google_price_level": str | None,
|
|
48
|
+
"google_address": str,
|
|
49
|
+
"place_id": str,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
On no match / missing entitlement / upstream error::
|
|
53
|
+
|
|
54
|
+
{"found": False} # clean miss
|
|
55
|
+
{"found": False, "reason": "not_configured"} # no entitlement
|
|
56
|
+
{"found": False, "error": "..."} # upstream error
|
|
57
|
+
|
|
58
|
+
Example::
|
|
59
|
+
|
|
60
|
+
result = client.places.search(
|
|
61
|
+
name="Corazon Mexicano",
|
|
62
|
+
lat=37.7647,
|
|
63
|
+
lon=-122.4194,
|
|
64
|
+
)
|
|
65
|
+
if result["found"]:
|
|
66
|
+
print(f"{result['google_name']}: {result['google_rating']}★")
|
|
67
|
+
"""
|
|
68
|
+
return self._client.get(
|
|
69
|
+
"/api/v1/sdk/places/search",
|
|
70
|
+
params={"name": name, "lat": lat, "lon": lon},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def details(self, place_id: str) -> dict:
|
|
74
|
+
"""Fetch place details (reviews, website, opening hours) by place_id.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
place_id: Google Places place_id (from a prior ``search()`` call).
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
On success::
|
|
81
|
+
|
|
82
|
+
{
|
|
83
|
+
"found": True,
|
|
84
|
+
"reviews": [
|
|
85
|
+
{
|
|
86
|
+
"author_name": str,
|
|
87
|
+
"rating": float | None,
|
|
88
|
+
"text": str,
|
|
89
|
+
"relative_time_description": str,
|
|
90
|
+
},
|
|
91
|
+
... # up to 5 reviews
|
|
92
|
+
],
|
|
93
|
+
"website": str | None,
|
|
94
|
+
"google_maps_url": str | None,
|
|
95
|
+
"opening_hours": dict,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
On missing entitlement or upstream error::
|
|
99
|
+
|
|
100
|
+
{"found": False, ...}
|
|
101
|
+
|
|
102
|
+
Reviews are capped at 5 per call; each review ``text`` is capped at
|
|
103
|
+
500 chars. If you need the full Google review text, call the Places
|
|
104
|
+
API directly (which you'd need if you're running your own backend
|
|
105
|
+
anyway — but the SDK keeps it simple for chat/display use cases).
|
|
106
|
+
|
|
107
|
+
Example::
|
|
108
|
+
|
|
109
|
+
details = client.places.details(result["place_id"])
|
|
110
|
+
for r in details.get("reviews", []):
|
|
111
|
+
print(f"{r['author_name']} ({r['rating']}★): {r['text'][:80]}")
|
|
112
|
+
"""
|
|
113
|
+
return self._client.get(f"/api/v1/sdk/places/details/{place_id}")
|
|
114
|
+
|
|
115
|
+
def streetview(self, lat: float, lon: float) -> dict:
|
|
116
|
+
"""Return a Google Maps Street View deep-link URL for the coordinates.
|
|
117
|
+
|
|
118
|
+
This endpoint does NOT call the Google Places API — the URL format is
|
|
119
|
+
a public Google Maps deep-link convention and requires no API key.
|
|
120
|
+
Google handles "no imagery at the exact location" by finding the
|
|
121
|
+
nearest available panorama automatically.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
lat: Latitude.
|
|
125
|
+
lon: Longitude.
|
|
126
|
+
|
|
127
|
+
Returns::
|
|
128
|
+
|
|
129
|
+
{
|
|
130
|
+
"image_url": "", # future: Street View Static image
|
|
131
|
+
"link_url": "https://www.google.com/maps/@?api=1&map_action=pano&viewpoint={lat},{lon}",
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
Example::
|
|
135
|
+
|
|
136
|
+
sv = client.places.streetview(37.7647, -122.4194)
|
|
137
|
+
# Open sv["link_url"] in a new tab to show Street View.
|
|
138
|
+
"""
|
|
139
|
+
return self._client.get(
|
|
140
|
+
"/api/v1/sdk/places/streetview",
|
|
141
|
+
params={"lat": lat, "lon": lon},
|
|
142
|
+
)
|
workweek/teams.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Teams module — team management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from workweek.client import WorkWeekClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TeamsModule:
|
|
11
|
+
def __init__(self, client: WorkWeekClient):
|
|
12
|
+
self._client = client
|
|
13
|
+
|
|
14
|
+
def list(self) -> dict:
|
|
15
|
+
"""List teams."""
|
|
16
|
+
return self._client.get("/api/v1/teams")
|
|
17
|
+
|
|
18
|
+
def get(self, team_id: int) -> dict:
|
|
19
|
+
"""Get team details."""
|
|
20
|
+
return self._client.get(f"/api/v1/teams/{team_id}")
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: workweek
|
|
3
|
+
Version: 0.4.2
|
|
4
|
+
Summary: Official Python SDK for the WorkWeek platform — query datasets, run agents, chat, and embed WorkWeek capabilities into your apps.
|
|
5
|
+
Author: WorkWeek
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://workweek.io
|
|
8
|
+
Project-URL: Documentation, https://docs.workweek.io
|
|
9
|
+
Project-URL: Issues, https://github.com/rchow93/workweek-sdk-python/issues
|
|
10
|
+
Keywords: workweek,sdk,ai,agents,iceberg,data
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: httpx>=0.27.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
27
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# WorkWeek Python SDK
|
|
31
|
+
|
|
32
|
+
Official Python SDK for the [WorkWeek](https://workweek.io) platform.
|
|
33
|
+
|
|
34
|
+
Embed WorkWeek's data, agents, and chat capabilities into your own Python apps with
|
|
35
|
+
a thin, dependency-light HTTP client. Single runtime dependency: `httpx`.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
From git (until published to PyPI):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install "workweek @ git+https://github.com/rchow93/workweek-sdk-python.git@v0.2.2"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For local development:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/rchow93/workweek-sdk-python.git
|
|
49
|
+
cd workweek-sdk-python
|
|
50
|
+
pip install -e .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quickstart
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from workweek import WorkWeekClient
|
|
57
|
+
|
|
58
|
+
client = WorkWeekClient(
|
|
59
|
+
base_url="https://gw.askvai.com",
|
|
60
|
+
api_key="wk_rpt_...", # Get from Settings → API Keys in the WorkWeek portal
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Query an Iceberg dataset
|
|
64
|
+
result = client.data.query(
|
|
65
|
+
dataset="sf_food_trucks_permits",
|
|
66
|
+
sql="SELECT COUNT(*) AS n FROM tbl WHERE status = 'APPROVED'",
|
|
67
|
+
)
|
|
68
|
+
print(result["rows"]) # → [{"n": 163}]
|
|
69
|
+
|
|
70
|
+
# Stream a chat message (v0.2.0+)
|
|
71
|
+
for event in client.chat.stream("How many food trucks are active in SF?"):
|
|
72
|
+
if event.type == "token":
|
|
73
|
+
print(event.content, end="", flush=True)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Authentication
|
|
77
|
+
|
|
78
|
+
All requests use API key authentication via the `X-API-Key` header. Create a key
|
|
79
|
+
in the WorkWeek portal under **Settings → API Keys**. Pass it to `WorkWeekClient`:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
client = WorkWeekClient(base_url="https://gw.askvai.com", api_key="wk_rpt_...")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The org and user context are derived from the API key — never pass `org_id` from
|
|
86
|
+
the client.
|
|
87
|
+
|
|
88
|
+
## Modules
|
|
89
|
+
|
|
90
|
+
| Module | Purpose |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `client.data` | Query Iceberg datasets via SQL, list available datasets, inspect schemas |
|
|
93
|
+
| `client.chat` | Conversational AI with tool-calling and streaming responses |
|
|
94
|
+
| `client.apps` | Tenant app config, page data, dashboard frontend management |
|
|
95
|
+
| `client.agents` | Saved agent CRUD |
|
|
96
|
+
| `client.teams` | Team listing |
|
|
97
|
+
| `client.knowledge` | Knowledge collection search |
|
|
98
|
+
| `client.analysis` | BACI and statistical analysis |
|
|
99
|
+
| `client.execution` | Submit and track agent executions |
|
|
100
|
+
|
|
101
|
+
## Error Handling
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from workweek import WorkWeekAPIError
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
result = client.data.query(dataset="missing", sql="SELECT 1")
|
|
108
|
+
except WorkWeekAPIError as e:
|
|
109
|
+
print(f"HTTP {e.status_code}: {e.message}")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Requirements
|
|
113
|
+
|
|
114
|
+
- Python 3.10+
|
|
115
|
+
- `httpx>=0.27.0`
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
Apache License 2.0 — see [LICENSE](LICENSE).
|
|
120
|
+
|
|
121
|
+
## Status
|
|
122
|
+
|
|
123
|
+
Alpha. API may change before 1.0. Pin to a specific version in production:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
pip install "workweek @ git+https://github.com/rchow93/workweek-sdk-python.git@v0.2.2"
|
|
127
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
workweek/__init__.py,sha256=4lJG3URDpd-NlbdGB6d-uOOb7clhC8-GsmTwfd-4vX8,210
|
|
2
|
+
workweek/agents.py,sha256=1h7fa37lcaSiYrfVUOALWbKKoMXdmeHCW-5gyB8SkzY,746
|
|
3
|
+
workweek/analysis.py,sha256=4ME28X0wQvmUCYFCW6ctt1dgr7La-6xNIY-w57qztjg,504
|
|
4
|
+
workweek/apps.py,sha256=Ik573pWvukCnOpSyff3RrVyBXlVFleWYEwPlQiqpFMA,1543
|
|
5
|
+
workweek/chat.py,sha256=sZxV4MsXNgcEUqwLIXVcX1JxOdnRs4c_7fbDUAPorLk,6838
|
|
6
|
+
workweek/client.py,sha256=iwMKD7s9y6mHwIGDQ0ZYh9BrZ7PIP_i2dNlvzJaUbBE,3544
|
|
7
|
+
workweek/data.py,sha256=GGBVgIEUNq6fWq2TAWcM2R3IR0WL5cChVIigiXTKE2I,2380
|
|
8
|
+
workweek/execution.py,sha256=yX2jDvnKilA4FH3uoG7m3Ks92jT404vbKQQxtkGNr74,4702
|
|
9
|
+
workweek/knowledge.py,sha256=ba7wK5KclZrU9K5UsWPBqpwz0dwTUMVrJTZYQ0o-N_8,743
|
|
10
|
+
workweek/places.py,sha256=RpM0xyyKJvrodHx3P2AMV7OfFtVguH4qPwT6DjY725o,5220
|
|
11
|
+
workweek/teams.py,sha256=2NA-eNUANNAKeYf1tG4il3vjsW1w7MmrBxB6J4UMx9E,512
|
|
12
|
+
workweek-0.4.2.dist-info/licenses/LICENSE,sha256=epJksMdTXjvUX6Vlijyb2wucKk6UDCqqu8b6KcS5QUc,10695
|
|
13
|
+
workweek-0.4.2.dist-info/METADATA,sha256=X5mgqrPrsTJHEe13LJ0ah83nYRodO9OyM2xO5LnYATk,3794
|
|
14
|
+
workweek-0.4.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
15
|
+
workweek-0.4.2.dist-info/top_level.txt,sha256=aarHybc0oh7oaoCQdB8gDlk58utqSIs88sEUKc923W8,9
|
|
16
|
+
workweek-0.4.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for describing the origin of the Work and
|
|
141
|
+
reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Support. While redistributing the Work or
|
|
166
|
+
Derivative Works thereof, You may choose to offer, and charge a
|
|
167
|
+
fee for, acceptance of support, warranty, indemnity, or other
|
|
168
|
+
liability obligations and/or rights consistent with this License.
|
|
169
|
+
However, in accepting such obligations, You may act only on Your
|
|
170
|
+
own behalf and on Your sole responsibility, not on behalf of any
|
|
171
|
+
other Contributor, and only if You agree to indemnify, defend, and
|
|
172
|
+
hold each Contributor harmless for any liability incurred by, or
|
|
173
|
+
claims asserted against, such Contributor by reason of your
|
|
174
|
+
accepting any such warranty or support.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
Copyright 2026 WorkWeek
|
|
179
|
+
|
|
180
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
181
|
+
you may not use this file except in compliance with the License.
|
|
182
|
+
You may obtain a copy of the License at
|
|
183
|
+
|
|
184
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
185
|
+
|
|
186
|
+
Unless required by applicable law or agreed to in writing, software
|
|
187
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
188
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
189
|
+
See the License for the specific language governing permissions and
|
|
190
|
+
limitations under the License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
workweek
|