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 ADDED
@@ -0,0 +1,6 @@
1
+ """WorkWeek Python SDK — thin HTTP wrapper for tenant app integration."""
2
+
3
+ from workweek.client import WorkWeekClient, WorkWeekAPIError
4
+
5
+ __all__ = ["WorkWeekClient", "WorkWeekAPIError"]
6
+ __version__ = "0.4.2"
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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