convexity-sdk 0.12.0.dev238__tar.gz → 0.12.0.dev239__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.
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/PKG-INFO +1 -1
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/organizations.py +3 -3
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/projects.py +7 -1
- convexity_sdk-0.12.0.dev239/samples/README.md +31 -0
- convexity_sdk-0.12.0.dev239/samples/create_org_and_project.py +78 -0
- convexity_sdk-0.12.0.dev239/samples/quickbook/001_check_quickbook_connection.py +142 -0
- convexity_sdk-0.12.0.dev239/samples/quickbook/002_create_quickbook_datasets.py +266 -0
- convexity_sdk-0.12.0.dev239/samples/shopify/001_check_shopify_connection.py +133 -0
- convexity_sdk-0.12.0.dev239/samples/shopify/002_create_shopify_datasets.py +262 -0
- convexity_sdk-0.12.0.dev238/samples/README.md +0 -27
- convexity_sdk-0.12.0.dev238/samples/create_org_and_project.py +0 -51
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/.gitignore +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/README.md +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/__init__.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/client.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/config.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/exceptions.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/http.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/models/__init__.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/__init__.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/api_keys.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/connections.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/dashboards.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/datalake.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/datasets.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/environments.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/issues.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/runs.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/tasks.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/usage.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/workspace.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/pyproject.toml +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/.env.example +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/cancel_refresh_task.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/compare_api_key_scopes.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/create_csv_datasets.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/create_dashboards.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/create_databricks_connections.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/create_datasets.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/create_forecast_task.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/create_single_project_api_key.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/getting_started.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/list_orgs_and_projects.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/manage_environments.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/manage_issues.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/project_usage.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/query_datalake.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/share_dashboard.py +0 -0
- {convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/workspace_files.py +0 -0
{convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/organizations.py
RENAMED
|
@@ -69,12 +69,12 @@ class Organizations:
|
|
|
69
69
|
self,
|
|
70
70
|
organization_id: str,
|
|
71
71
|
*,
|
|
72
|
-
|
|
72
|
+
email: str,
|
|
73
73
|
role: OrganizationRole = OrganizationRole.MEMBER,
|
|
74
74
|
) -> OrganizationMemberResponse:
|
|
75
|
-
"""Add a member to an organization."""
|
|
75
|
+
"""Add a member to an organization by email."""
|
|
76
76
|
body = AddOrganizationMemberRequest(
|
|
77
|
-
|
|
77
|
+
email=email,
|
|
78
78
|
role=role,
|
|
79
79
|
)
|
|
80
80
|
result = _add_member.sync(organization_id, client=self._client, body=body)
|
{convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/projects.py
RENAMED
|
@@ -18,6 +18,7 @@ from convexity_api_client.api.v1 import (
|
|
|
18
18
|
from convexity_api_client.api.v1 import (
|
|
19
19
|
list_user_projects_v1_projects_get as _list_projects,
|
|
20
20
|
)
|
|
21
|
+
from convexity_api_client.errors import UnexpectedStatus
|
|
21
22
|
from convexity_api_client.models.create_project_request import CreateProjectRequest
|
|
22
23
|
from convexity_api_client.models.project_list_response import ProjectListResponse
|
|
23
24
|
from convexity_api_client.models.project_response import ProjectResponse
|
|
@@ -56,7 +57,12 @@ class Projects:
|
|
|
56
57
|
|
|
57
58
|
def get_by_slug(self, organization_id: str, slug: str) -> ProjectResponse | None:
|
|
58
59
|
"""Get a single project by slug within an organization."""
|
|
59
|
-
|
|
60
|
+
try:
|
|
61
|
+
result = _get_project_by_slug.sync(organization_id, slug, client=self._client)
|
|
62
|
+
except UnexpectedStatus as exc:
|
|
63
|
+
if exc.status_code == 404:
|
|
64
|
+
return None
|
|
65
|
+
raise
|
|
60
66
|
if isinstance(result, ProjectResponse):
|
|
61
67
|
return result
|
|
62
68
|
return None
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
git pull# Convexity SDK Samples
|
|
2
|
+
|
|
3
|
+
This directory contains sample scripts demonstrating how to use the Convexity SDK.
|
|
4
|
+
|
|
5
|
+
## Samples
|
|
6
|
+
|
|
7
|
+
| Sample | Description |
|
|
8
|
+
| ------------------------------------------------------------------------------------------ | ------------------------------------------------------- |
|
|
9
|
+
| [getting_started.py](getting_started.py) | Basic SDK setup and usage |
|
|
10
|
+
| [create_org_and_project.py](create_org_and_project.py) | Create an organization and project |
|
|
11
|
+
| [create_databricks_connections.py](create_databricks_connections.py) | Create Databricks connections |
|
|
12
|
+
| [create_datasets.py](create_datasets.py) | Create datasets from Databricks connections |
|
|
13
|
+
| [create_dashboards.py](create_dashboards.py) | Create analytic dashboards with widgets |
|
|
14
|
+
| [share_dashboard.py](share_dashboard.py) | Share a dashboard with other users |
|
|
15
|
+
| [list_orgs_and_projects.py](list_orgs_and_projects.py) | List organizations and projects |
|
|
16
|
+
| [query_datalake.py](query_datalake.py) | Query the DuckLake warehouse with DuckDB |
|
|
17
|
+
| [cancel_refresh_task.py](cancel_refresh_task.py) | Cancel a pending dataset refresh task |
|
|
18
|
+
| [create_forecast_task.py](create_forecast_task.py) | Create a demand-forecasting task and run it |
|
|
19
|
+
| [compare_api_key_scopes.py](compare_api_key_scopes.py) | Compare READ vs ADMIN API key behavior |
|
|
20
|
+
| [debug_api_key_tenant_context.py](debug_api_key_tenant_context.py) | Diagnose API key tenant/scope mismatch |
|
|
21
|
+
| [quickbook/001_check_quickbook_connection.py](quickbook/001_check_quickbook_connection.py) | Verify a QuickBooks connection before creating datasets |
|
|
22
|
+
| [quickbook/002_create_quickbook_datasets.py](quickbook/002_create_quickbook_datasets.py) | Create and refresh QuickBooks-backed datasets |
|
|
23
|
+
| [shopify/001_check_shopify_connection.py](shopify/001_check_shopify_connection.py) | Verify a Shopify connection before creating datasets |
|
|
24
|
+
| [shopify/002_create_shopify_datasets.py](shopify/002_create_shopify_datasets.py) | Create and refresh Shopify-backed datasets |
|
|
25
|
+
|
|
26
|
+
## Running Samples
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# From the repo root
|
|
30
|
+
uv run python packages/convexity-sdk/samples/getting_started.py
|
|
31
|
+
```
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Convexity SDK - Create Organization & Project
|
|
3
|
+
|
|
4
|
+
This sample demonstrates how to:
|
|
5
|
+
- Create or reuse an organization
|
|
6
|
+
- Optionally add members to the organization as admins by email
|
|
7
|
+
- Create or reuse a project within the organization
|
|
8
|
+
|
|
9
|
+
Prerequisites:
|
|
10
|
+
- A valid API key with WRITE or ADMIN permission
|
|
11
|
+
|
|
12
|
+
Optional environment variables:
|
|
13
|
+
- CONVEXITY_SAMPLE_ADMIN_EMAILS: comma-separated emails to add as admins
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
|
|
18
|
+
from convexity_api_client.errors import UnexpectedStatus
|
|
19
|
+
from convexity_api_client.models.organization_role import OrganizationRole
|
|
20
|
+
|
|
21
|
+
from convexity_sdk import ConvexityClient
|
|
22
|
+
|
|
23
|
+
ORG_SLUG = "convexity-sdk-samples"
|
|
24
|
+
PROJECT_SLUG = "griffin-it"
|
|
25
|
+
ADMIN_EMAILS = [email.strip() for email in os.getenv("CONVEXITY_SAMPLE_ADMIN_EMAILS", "").split(",") if email.strip()]
|
|
26
|
+
|
|
27
|
+
client = ConvexityClient()
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
# ── Step 1: Create or reuse an organization ─────────────────────────
|
|
31
|
+
org = client.organizations.get_by_slug(ORG_SLUG)
|
|
32
|
+
if org is None:
|
|
33
|
+
org = client.organizations.create(
|
|
34
|
+
name="Convexity SDK samples",
|
|
35
|
+
slug=ORG_SLUG,
|
|
36
|
+
description="Organization created by the Convexity SDK sample scripts",
|
|
37
|
+
)
|
|
38
|
+
print(f"Created organization: {org.name} (id={org.id})")
|
|
39
|
+
else:
|
|
40
|
+
print(f"Found organization: {org.name} (id={org.id})")
|
|
41
|
+
|
|
42
|
+
# ── Step 2: Optionally add admin members ────────────────────────────
|
|
43
|
+
if not ADMIN_EMAILS:
|
|
44
|
+
print("No admin emails configured; skipping member invite step.")
|
|
45
|
+
print("Set CONVEXITY_SAMPLE_ADMIN_EMAILS=email1@example.com,email2@example.com to add admins.")
|
|
46
|
+
|
|
47
|
+
for email in ADMIN_EMAILS:
|
|
48
|
+
try:
|
|
49
|
+
member = client.organizations.add_member(
|
|
50
|
+
org.id,
|
|
51
|
+
email=email,
|
|
52
|
+
role=OrganizationRole.ADMIN,
|
|
53
|
+
)
|
|
54
|
+
except UnexpectedStatus as exc:
|
|
55
|
+
detail = exc.content.decode(errors="ignore")
|
|
56
|
+
if exc.status_code == 400 and "already a member" in detail:
|
|
57
|
+
print(f" Admin already exists: {email}")
|
|
58
|
+
continue
|
|
59
|
+
print(f" Skipped admin {email}: {detail}")
|
|
60
|
+
continue
|
|
61
|
+
print(f" Added admin: {email} (user_id={member.user_id}, role={member.role})")
|
|
62
|
+
|
|
63
|
+
# ── Step 3: Create or reuse a project in the organization ───────────
|
|
64
|
+
project = client.projects.get_by_slug(org.id, PROJECT_SLUG)
|
|
65
|
+
if project is None:
|
|
66
|
+
project = client.projects.create(
|
|
67
|
+
name="griffin it",
|
|
68
|
+
slug=PROJECT_SLUG,
|
|
69
|
+
organization_id=org.id,
|
|
70
|
+
description="Test project created by the Convexity SDK sample scripts",
|
|
71
|
+
)
|
|
72
|
+
print(f"Created project: {project.name} (id={project.id})")
|
|
73
|
+
else:
|
|
74
|
+
print(f"Found project: {project.name} (id={project.id})")
|
|
75
|
+
|
|
76
|
+
print("\nDone!")
|
|
77
|
+
finally:
|
|
78
|
+
client.close()
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Convexity SDK - Check QuickBooks Connection
|
|
3
|
+
|
|
4
|
+
This sample verifies that a QuickBooks connection exists for a project and can
|
|
5
|
+
connect successfully. Run this before creating QuickBooks-backed datasets.
|
|
6
|
+
|
|
7
|
+
Prerequisites:
|
|
8
|
+
- A valid API key with READ permission for listing connections.
|
|
9
|
+
- WRITE or ADMIN permission if you want the sample to run the live verify step.
|
|
10
|
+
- An existing organization and project.
|
|
11
|
+
Run ../create_org_and_project.py first if needed.
|
|
12
|
+
- A QuickBooks connection created manually in Convexity.
|
|
13
|
+
|
|
14
|
+
Optional environment variables:
|
|
15
|
+
- QUICKBOOKS_ORG_SLUG: organization slug, defaults to convexity-sdk-samples
|
|
16
|
+
- QUICKBOOKS_PROJECT_SLUG: project slug, defaults to griffin-it
|
|
17
|
+
- QUICKBOOKS_CONNECTION_NAME: when set, require this exact connection name
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from convexity_sdk import ConvexityClient
|
|
27
|
+
from convexity_sdk.exceptions import ConvexityError
|
|
28
|
+
|
|
29
|
+
ORG_SLUG = os.getenv("QUICKBOOKS_ORG_SLUG", "convexity-sdk-samples")
|
|
30
|
+
PROJECT_SLUG = os.getenv("QUICKBOOKS_PROJECT_SLUG", "griffin-it")
|
|
31
|
+
QUICKBOOKS_CONNECTION_NAME = os.getenv("QUICKBOOKS_CONNECTION_NAME")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def find_project_id(client: ConvexityClient) -> str:
|
|
35
|
+
org = client.organizations.get_by_slug(ORG_SLUG)
|
|
36
|
+
if org is None:
|
|
37
|
+
print(f"Organization '{ORG_SLUG}' not found. Run ../create_org_and_project.py first.", file=sys.stderr)
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
|
|
40
|
+
project = client.projects.get_by_slug(org.id, PROJECT_SLUG)
|
|
41
|
+
if project is None:
|
|
42
|
+
print(f"Project '{PROJECT_SLUG}' not found in organization '{ORG_SLUG}'.", file=sys.stderr)
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
|
|
45
|
+
print(f"Using project: {project.name} ({project.id})")
|
|
46
|
+
return project.id
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def connection_to_dict(connection: Any) -> dict[str, Any]:
|
|
50
|
+
if hasattr(connection, "to_dict"):
|
|
51
|
+
data = connection.to_dict()
|
|
52
|
+
if isinstance(data, dict):
|
|
53
|
+
return data
|
|
54
|
+
return {}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def find_quickbooks_connection(client: ConvexityClient, project_id: str) -> dict[str, Any] | None:
|
|
58
|
+
connections = client.connections.list(project_id=project_id).connections
|
|
59
|
+
quickbooks_connections = []
|
|
60
|
+
for connection in connections:
|
|
61
|
+
data = connection_to_dict(connection)
|
|
62
|
+
if data.get("type") == "QUICKBOOKS":
|
|
63
|
+
quickbooks_connections.append(data)
|
|
64
|
+
|
|
65
|
+
if QUICKBOOKS_CONNECTION_NAME is not None:
|
|
66
|
+
return next(
|
|
67
|
+
(connection for connection in quickbooks_connections if connection.get("name") == QUICKBOOKS_CONNECTION_NAME),
|
|
68
|
+
None,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return quickbooks_connections[0] if quickbooks_connections else None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def ask_user_to_create_connection(project_id: str) -> None:
|
|
75
|
+
print("QuickBooks connection is not available for this project.", file=sys.stderr)
|
|
76
|
+
print("Please create and connect a QuickBooks connection manually in Convexity, then rerun this sample.", file=sys.stderr)
|
|
77
|
+
print(f"Project ID: {project_id}", file=sys.stderr)
|
|
78
|
+
if QUICKBOOKS_CONNECTION_NAME is not None:
|
|
79
|
+
print(f"Expected connection name: {QUICKBOOKS_CONNECTION_NAME}", file=sys.stderr)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def verify_quickbooks_connection(client: ConvexityClient, connection_id: str) -> dict[str, Any]:
|
|
83
|
+
response = client._http.request("post", f"/v1/connections/quickbooks/{connection_id}/verify")
|
|
84
|
+
data = response.json()
|
|
85
|
+
if not isinstance(data, dict):
|
|
86
|
+
raise ValueError(f"Unexpected QuickBooks verify response: {data!r}")
|
|
87
|
+
return data
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def main() -> None:
|
|
91
|
+
client = ConvexityClient()
|
|
92
|
+
try:
|
|
93
|
+
print(f"Connected to {client.base_url}")
|
|
94
|
+
project_id = find_project_id(client)
|
|
95
|
+
|
|
96
|
+
connection = find_quickbooks_connection(client, project_id)
|
|
97
|
+
if connection is None:
|
|
98
|
+
ask_user_to_create_connection(project_id)
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
|
|
101
|
+
connection_id = connection.get("id")
|
|
102
|
+
if not isinstance(connection_id, str) or not connection_id:
|
|
103
|
+
print(f"QuickBooks connection '{connection.get('name')}' has no connection ID.", file=sys.stderr)
|
|
104
|
+
ask_user_to_create_connection(project_id)
|
|
105
|
+
sys.exit(1)
|
|
106
|
+
|
|
107
|
+
print(f"Found QuickBooks connection: {connection.get('name')} ({connection_id})")
|
|
108
|
+
if connection.get("realmId"):
|
|
109
|
+
print(f"Realm ID: {connection['realmId']}")
|
|
110
|
+
if connection.get("environment"):
|
|
111
|
+
print(f"Environment: {connection['environment']}")
|
|
112
|
+
if connection.get("status"):
|
|
113
|
+
print(f"Stored status: {connection['status']}")
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
verified = verify_quickbooks_connection(client, connection_id)
|
|
117
|
+
except ConvexityError as exc:
|
|
118
|
+
print(f"QuickBooks verification failed: {exc}", file=sys.stderr)
|
|
119
|
+
print("Please reconnect or recreate the QuickBooks connection manually in Convexity, then rerun this sample.", file=sys.stderr)
|
|
120
|
+
sys.exit(1)
|
|
121
|
+
|
|
122
|
+
status = verified.get("status")
|
|
123
|
+
if status != "CONNECTED":
|
|
124
|
+
print(f"QuickBooks connection is not connected. Current status: {status or 'unknown'}", file=sys.stderr)
|
|
125
|
+
if verified.get("errorMessage"):
|
|
126
|
+
print(f"Error: {verified['errorMessage']}", file=sys.stderr)
|
|
127
|
+
print("Please reconnect or recreate the QuickBooks connection manually in Convexity, then rerun this sample.", file=sys.stderr)
|
|
128
|
+
sys.exit(1)
|
|
129
|
+
|
|
130
|
+
print("QuickBooks connection is available and connected.")
|
|
131
|
+
if verified.get("realmId"):
|
|
132
|
+
print(f"Realm ID: {verified['realmId']}")
|
|
133
|
+
if verified.get("oauthScopes"):
|
|
134
|
+
print("OAuth scopes:")
|
|
135
|
+
for scope in verified["oauthScopes"]:
|
|
136
|
+
print(f" - {scope}")
|
|
137
|
+
finally:
|
|
138
|
+
client.close()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
main()
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Convexity SDK - Create QuickBooks Datasets
|
|
3
|
+
|
|
4
|
+
This sample creates QuickBooks-backed datasets from an existing QuickBooks
|
|
5
|
+
connection, refreshes them, and prints a small sanity check for row count and
|
|
6
|
+
schema.
|
|
7
|
+
|
|
8
|
+
Prerequisites:
|
|
9
|
+
- Run 001_check_quickbook_connection.py first.
|
|
10
|
+
- A valid API key with WRITE or ADMIN permission.
|
|
11
|
+
- A connected QuickBooks connection in the target project.
|
|
12
|
+
|
|
13
|
+
Optional environment variables:
|
|
14
|
+
- QUICKBOOKS_ORG_SLUG: organization slug, defaults to convexity-sdk-samples
|
|
15
|
+
- QUICKBOOKS_PROJECT_SLUG: project slug, defaults to griffin-it
|
|
16
|
+
- QUICKBOOKS_CONNECTION_NAME: when set, require this exact connection name
|
|
17
|
+
- QUICKBOOKS_DATASET_ENTITIES: comma-separated QuickBooks entities, defaults to all supported entities
|
|
18
|
+
- QUICKBOOKS_REFRESH_TIMEOUT_SECONDS: refresh timeout per dataset, defaults to 180
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from convexity_api_client.errors import UnexpectedStatus
|
|
29
|
+
from convexity_api_client.models.dataset_config import DatasetConfig
|
|
30
|
+
from convexity_api_client.models.dataset_definition import DatasetDefinition
|
|
31
|
+
from convexity_api_client.models.dataset_type import DatasetType
|
|
32
|
+
from convexity_api_client.models.quick_books_dataset_config import QuickBooksDatasetConfig
|
|
33
|
+
from convexity_api_client.models.quick_books_entity import QuickBooksEntity
|
|
34
|
+
from convexity_api_client.models.refresh_dataset_payload import RefreshDatasetPayload
|
|
35
|
+
from convexity_api_client.models.refresh_mode import RefreshMode
|
|
36
|
+
from convexity_api_client.models.worker_task_status import WorkerTaskStatus
|
|
37
|
+
from convexity_api_client.types import Unset
|
|
38
|
+
|
|
39
|
+
from convexity_sdk import ConvexityClient
|
|
40
|
+
from convexity_sdk.exceptions import ConvexityError
|
|
41
|
+
|
|
42
|
+
ORG_SLUG = os.getenv("QUICKBOOKS_ORG_SLUG", "convexity-sdk-samples")
|
|
43
|
+
PROJECT_SLUG = os.getenv("QUICKBOOKS_PROJECT_SLUG", "griffin-it")
|
|
44
|
+
QUICKBOOKS_CONNECTION_NAME = os.getenv("QUICKBOOKS_CONNECTION_NAME")
|
|
45
|
+
DEFAULT_QUICKBOOKS_DATASET_ENTITIES = ",".join(entity.value for entity in QuickBooksEntity)
|
|
46
|
+
QUICKBOOKS_DATASET_ENTITIES = os.getenv("QUICKBOOKS_DATASET_ENTITIES", DEFAULT_QUICKBOOKS_DATASET_ENTITIES)
|
|
47
|
+
REFRESH_TIMEOUT_SECONDS = int(os.getenv("QUICKBOOKS_REFRESH_TIMEOUT_SECONDS", "180"))
|
|
48
|
+
POLL_INTERVAL_SECONDS = 5
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_entities() -> list[QuickBooksEntity]:
|
|
52
|
+
entities: list[QuickBooksEntity] = []
|
|
53
|
+
for raw_entity in QUICKBOOKS_DATASET_ENTITIES.split(","):
|
|
54
|
+
entity_name = raw_entity.strip()
|
|
55
|
+
if not entity_name:
|
|
56
|
+
continue
|
|
57
|
+
try:
|
|
58
|
+
entities.append(QuickBooksEntity(entity_name))
|
|
59
|
+
except ValueError:
|
|
60
|
+
valid = ", ".join(entity.value for entity in QuickBooksEntity)
|
|
61
|
+
print(f"Unknown QuickBooks entity '{entity_name}'. Valid values: {valid}", file=sys.stderr)
|
|
62
|
+
sys.exit(1)
|
|
63
|
+
if not entities:
|
|
64
|
+
print("QUICKBOOKS_DATASET_ENTITIES did not include any entities.", file=sys.stderr)
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
return entities
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def find_project_id(client: ConvexityClient) -> str:
|
|
70
|
+
org = client.organizations.get_by_slug(ORG_SLUG)
|
|
71
|
+
if org is None:
|
|
72
|
+
print(f"Organization '{ORG_SLUG}' not found. Run ../create_org_and_project.py first.", file=sys.stderr)
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
|
|
75
|
+
project = client.projects.get_by_slug(org.id, PROJECT_SLUG)
|
|
76
|
+
if project is None:
|
|
77
|
+
print(f"Project '{PROJECT_SLUG}' not found in organization '{ORG_SLUG}'.", file=sys.stderr)
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
|
|
80
|
+
print(f"Using project: {project.name} ({project.id})")
|
|
81
|
+
return project.id
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def connection_to_dict(connection: Any) -> dict[str, Any]:
|
|
85
|
+
if hasattr(connection, "to_dict"):
|
|
86
|
+
data = connection.to_dict()
|
|
87
|
+
if isinstance(data, dict):
|
|
88
|
+
return data
|
|
89
|
+
return {}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def find_quickbooks_connection(client: ConvexityClient, project_id: str) -> dict[str, Any] | None:
|
|
93
|
+
connections = client.connections.list(project_id=project_id).connections
|
|
94
|
+
quickbooks_connections = []
|
|
95
|
+
for connection in connections:
|
|
96
|
+
data = connection_to_dict(connection)
|
|
97
|
+
if data.get("type") == "QUICKBOOKS":
|
|
98
|
+
quickbooks_connections.append(data)
|
|
99
|
+
|
|
100
|
+
if QUICKBOOKS_CONNECTION_NAME is not None:
|
|
101
|
+
return next(
|
|
102
|
+
(connection for connection in quickbooks_connections if connection.get("name") == QUICKBOOKS_CONNECTION_NAME),
|
|
103
|
+
None,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return quickbooks_connections[0] if quickbooks_connections else None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def verify_quickbooks_connection(client: ConvexityClient, connection_id: str) -> dict[str, Any]:
|
|
110
|
+
response = client._http.request("post", f"/v1/connections/quickbooks/{connection_id}/verify")
|
|
111
|
+
data = response.json()
|
|
112
|
+
if not isinstance(data, dict):
|
|
113
|
+
raise ValueError(f"Unexpected QuickBooks verify response: {data!r}")
|
|
114
|
+
return data
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def dataset_name_for(entity: QuickBooksEntity) -> str:
|
|
118
|
+
return f"quickbooks_{entity.value.lower()}"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def build_config(entity: QuickBooksEntity) -> DatasetConfig:
|
|
122
|
+
return DatasetConfig(quickbooks=QuickBooksDatasetConfig(entity=entity))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def find_existing_dataset(client: ConvexityClient, project_id: str, name: str) -> DatasetDefinition | None:
|
|
126
|
+
datasets = client.datasets.list(project_id).datasets
|
|
127
|
+
return next((dataset for dataset in datasets if dataset.name == name), None)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def create_or_update_dataset(
|
|
131
|
+
client: ConvexityClient,
|
|
132
|
+
*,
|
|
133
|
+
project_id: str,
|
|
134
|
+
connection_id: str,
|
|
135
|
+
entity: QuickBooksEntity,
|
|
136
|
+
) -> DatasetDefinition:
|
|
137
|
+
name = dataset_name_for(entity)
|
|
138
|
+
config = build_config(entity)
|
|
139
|
+
existing = find_existing_dataset(client, project_id, name)
|
|
140
|
+
if existing is not None:
|
|
141
|
+
dataset = client.datasets.update(
|
|
142
|
+
existing.id,
|
|
143
|
+
connection_id=connection_id,
|
|
144
|
+
config=config,
|
|
145
|
+
refresh_mode=RefreshMode.MANUAL,
|
|
146
|
+
)
|
|
147
|
+
print(f"Updated dataset: {dataset.name} ({dataset.id})")
|
|
148
|
+
return dataset
|
|
149
|
+
|
|
150
|
+
dataset = client.datasets.create(
|
|
151
|
+
project_id=project_id,
|
|
152
|
+
name=name,
|
|
153
|
+
type_=DatasetType.QUICKBOOKS,
|
|
154
|
+
config=config,
|
|
155
|
+
connection_id=connection_id,
|
|
156
|
+
refresh_mode=RefreshMode.MANUAL,
|
|
157
|
+
description=f"QuickBooks {entity.value} dataset created by the SDK sample",
|
|
158
|
+
tags=["quickbooks", "sdk-sample"],
|
|
159
|
+
)
|
|
160
|
+
print(f"Created dataset: {dataset.name} ({dataset.id})")
|
|
161
|
+
return dataset
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def refresh_and_wait(client: ConvexityClient, dataset: DatasetDefinition) -> RefreshDatasetPayload:
|
|
165
|
+
try:
|
|
166
|
+
task = client.datasets.refresh(dataset.id)
|
|
167
|
+
except (UnexpectedStatus, ConvexityError, ValueError) as exc:
|
|
168
|
+
raise RuntimeError(f"Failed to trigger refresh for '{dataset.name}': {exc}") from exc
|
|
169
|
+
|
|
170
|
+
print(f" Refresh started (task_id={task.id})", end="", flush=True)
|
|
171
|
+
deadline = time.monotonic() + REFRESH_TIMEOUT_SECONDS
|
|
172
|
+
while True:
|
|
173
|
+
if time.monotonic() > deadline:
|
|
174
|
+
raise TimeoutError(f"Refresh timed out for '{dataset.name}' after {REFRESH_TIMEOUT_SECONDS} seconds")
|
|
175
|
+
|
|
176
|
+
time.sleep(POLL_INTERVAL_SECONDS)
|
|
177
|
+
state = client.datasets.get_refresh_status(dataset.id, task.id)
|
|
178
|
+
payload = state.payload
|
|
179
|
+
if isinstance(payload, RefreshDatasetPayload):
|
|
180
|
+
message = payload.progress_message if not isinstance(payload.progress_message, Unset) else None
|
|
181
|
+
if message:
|
|
182
|
+
print(f" {message}", end="", flush=True)
|
|
183
|
+
else:
|
|
184
|
+
payload = None
|
|
185
|
+
|
|
186
|
+
if state.status == WorkerTaskStatus.COMPLETED:
|
|
187
|
+
print(" COMPLETED")
|
|
188
|
+
if payload is None:
|
|
189
|
+
raise ValueError(f"Refresh completed for '{dataset.name}' without a refresh payload")
|
|
190
|
+
return payload
|
|
191
|
+
|
|
192
|
+
if state.status == WorkerTaskStatus.FAILED:
|
|
193
|
+
error = state.error if not isinstance(state.error, Unset) else "unknown error"
|
|
194
|
+
raise RuntimeError(f"Refresh failed for '{dataset.name}': {error}")
|
|
195
|
+
|
|
196
|
+
if state.status == WorkerTaskStatus.CANCELLED:
|
|
197
|
+
raise RuntimeError(f"Refresh was cancelled for '{dataset.name}'")
|
|
198
|
+
|
|
199
|
+
print(".", end="", flush=True)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def print_dataset_sanity(client: ConvexityClient, dataset: DatasetDefinition, payload: RefreshDatasetPayload) -> None:
|
|
203
|
+
refreshed = client.datasets.get(dataset.id)
|
|
204
|
+
if refreshed is None:
|
|
205
|
+
raise ValueError(f"Could not fetch refreshed dataset '{dataset.name}'")
|
|
206
|
+
|
|
207
|
+
row_count = payload.row_count if not isinstance(payload.row_count, Unset) else None
|
|
208
|
+
schema = refreshed.schema
|
|
209
|
+
columns = [] if schema is None or isinstance(schema, Unset) else schema
|
|
210
|
+
|
|
211
|
+
print(f" Rows written: {row_count if row_count is not None else 'unknown'}")
|
|
212
|
+
print(f" Columns: {len(columns)}")
|
|
213
|
+
for column in columns[:8]:
|
|
214
|
+
column_type = getattr(column, "type_", getattr(column, "type", "unknown"))
|
|
215
|
+
print(f" - {column.name}: {column_type}")
|
|
216
|
+
|
|
217
|
+
if row_count is None:
|
|
218
|
+
raise ValueError(f"Refresh for '{dataset.name}' did not report row_count")
|
|
219
|
+
if not columns:
|
|
220
|
+
raise ValueError(f"Refresh for '{dataset.name}' did not produce a schema")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def main() -> None:
|
|
224
|
+
entities = parse_entities()
|
|
225
|
+
client = ConvexityClient()
|
|
226
|
+
try:
|
|
227
|
+
print(f"Connected to {client.base_url}")
|
|
228
|
+
project_id = find_project_id(client)
|
|
229
|
+
|
|
230
|
+
connection = find_quickbooks_connection(client, project_id)
|
|
231
|
+
if connection is None:
|
|
232
|
+
print("QuickBooks connection is not available for this project.", file=sys.stderr)
|
|
233
|
+
print("Run 001_check_quickbook_connection.py after creating the connection manually.", file=sys.stderr)
|
|
234
|
+
sys.exit(1)
|
|
235
|
+
|
|
236
|
+
connection_id = connection.get("id")
|
|
237
|
+
if not isinstance(connection_id, str) or not connection_id:
|
|
238
|
+
print(f"QuickBooks connection '{connection.get('name')}' has no connection ID.", file=sys.stderr)
|
|
239
|
+
sys.exit(1)
|
|
240
|
+
|
|
241
|
+
verified = verify_quickbooks_connection(client, connection_id)
|
|
242
|
+
if verified.get("status") != "CONNECTED":
|
|
243
|
+
print("QuickBooks connection is not connected. Run 001_check_quickbook_connection.py first.", file=sys.stderr)
|
|
244
|
+
sys.exit(1)
|
|
245
|
+
|
|
246
|
+
print(f"Using QuickBooks connection: {connection.get('name')} ({connection_id})")
|
|
247
|
+
print("Entities:", ", ".join(entity.value for entity in entities))
|
|
248
|
+
|
|
249
|
+
for entity in entities:
|
|
250
|
+
print(f"\n-- {entity.value} --")
|
|
251
|
+
dataset = create_or_update_dataset(
|
|
252
|
+
client,
|
|
253
|
+
project_id=project_id,
|
|
254
|
+
connection_id=connection_id,
|
|
255
|
+
entity=entity,
|
|
256
|
+
)
|
|
257
|
+
payload = refresh_and_wait(client, dataset)
|
|
258
|
+
print_dataset_sanity(client, dataset, payload)
|
|
259
|
+
|
|
260
|
+
print("\nDone!")
|
|
261
|
+
finally:
|
|
262
|
+
client.close()
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
if __name__ == "__main__":
|
|
266
|
+
main()
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Convexity SDK - Check Shopify Connection
|
|
3
|
+
|
|
4
|
+
This sample verifies that a Shopify connection exists for a project and can
|
|
5
|
+
connect successfully. Run this before creating Shopify-backed datasets.
|
|
6
|
+
|
|
7
|
+
Prerequisites:
|
|
8
|
+
- A valid API key with READ permission for listing connections.
|
|
9
|
+
- WRITE or ADMIN permission if you want the sample to run the live verify step.
|
|
10
|
+
- An existing organization and project.
|
|
11
|
+
Run ../create_org_and_project.py first if needed.
|
|
12
|
+
- A Shopify connection created manually in Convexity.
|
|
13
|
+
|
|
14
|
+
Optional environment variables:
|
|
15
|
+
- SHOPIFY_ORG_SLUG: organization slug, defaults to convexity-sdk-samples
|
|
16
|
+
- SHOPIFY_PROJECT_SLUG: project slug, defaults to griffin-it
|
|
17
|
+
- SHOPIFY_CONNECTION_NAME: when set, require this exact connection name
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from convexity_sdk import ConvexityClient
|
|
27
|
+
from convexity_sdk.exceptions import ConvexityError
|
|
28
|
+
|
|
29
|
+
ORG_SLUG = os.getenv("SHOPIFY_ORG_SLUG", "convexity-sdk-samples")
|
|
30
|
+
PROJECT_SLUG = os.getenv("SHOPIFY_PROJECT_SLUG", "griffin-it")
|
|
31
|
+
SHOPIFY_CONNECTION_NAME = os.getenv("SHOPIFY_CONNECTION_NAME")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def find_project_id(client: ConvexityClient) -> str:
|
|
35
|
+
org = client.organizations.get_by_slug(ORG_SLUG)
|
|
36
|
+
if org is None:
|
|
37
|
+
print(f"Organization '{ORG_SLUG}' not found. Run ../create_org_and_project.py first.", file=sys.stderr)
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
|
|
40
|
+
project = client.projects.get_by_slug(org.id, PROJECT_SLUG)
|
|
41
|
+
if project is None:
|
|
42
|
+
print(f"Project '{PROJECT_SLUG}' not found in organization '{ORG_SLUG}'.", file=sys.stderr)
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
|
|
45
|
+
print(f"Using project: {project.name} ({project.id})")
|
|
46
|
+
return project.id
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def connection_to_dict(connection: Any) -> dict[str, Any]:
|
|
50
|
+
if hasattr(connection, "to_dict"):
|
|
51
|
+
data = connection.to_dict()
|
|
52
|
+
if isinstance(data, dict):
|
|
53
|
+
return data
|
|
54
|
+
return {}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def find_shopify_connection(client: ConvexityClient, project_id: str) -> dict[str, Any] | None:
|
|
58
|
+
connections = client.connections.list(project_id=project_id).connections
|
|
59
|
+
shopify_connections = [connection_to_dict(connection) for connection in connections if connection_to_dict(connection).get("type") == "SHOPIFY"]
|
|
60
|
+
|
|
61
|
+
if SHOPIFY_CONNECTION_NAME is not None:
|
|
62
|
+
return next((connection for connection in shopify_connections if connection.get("name") == SHOPIFY_CONNECTION_NAME), None)
|
|
63
|
+
|
|
64
|
+
return shopify_connections[0] if shopify_connections else None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def ask_user_to_create_connection(project_id: str) -> None:
|
|
68
|
+
print("Shopify connection is not available for this project.", file=sys.stderr)
|
|
69
|
+
print("Please create and connect a Shopify connection manually in Convexity, then rerun this sample.", file=sys.stderr)
|
|
70
|
+
print(f"Project ID: {project_id}", file=sys.stderr)
|
|
71
|
+
if SHOPIFY_CONNECTION_NAME is not None:
|
|
72
|
+
print(f"Expected connection name: {SHOPIFY_CONNECTION_NAME}", file=sys.stderr)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def verify_shopify_connection(client: ConvexityClient, connection_id: str) -> dict[str, Any]:
|
|
76
|
+
response = client._http.request("post", f"/v1/connections/shopify/{connection_id}/verify")
|
|
77
|
+
data = response.json()
|
|
78
|
+
if not isinstance(data, dict):
|
|
79
|
+
raise ValueError(f"Unexpected Shopify verify response: {data!r}")
|
|
80
|
+
return data
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def main() -> None:
|
|
84
|
+
client = ConvexityClient()
|
|
85
|
+
try:
|
|
86
|
+
print(f"Connected to {client.base_url}")
|
|
87
|
+
project_id = find_project_id(client)
|
|
88
|
+
|
|
89
|
+
connection = find_shopify_connection(client, project_id)
|
|
90
|
+
if connection is None:
|
|
91
|
+
ask_user_to_create_connection(project_id)
|
|
92
|
+
sys.exit(1)
|
|
93
|
+
|
|
94
|
+
connection_id = connection.get("id")
|
|
95
|
+
if not isinstance(connection_id, str) or not connection_id:
|
|
96
|
+
print(f"Shopify connection '{connection.get('name')}' has no connection ID.", file=sys.stderr)
|
|
97
|
+
ask_user_to_create_connection(project_id)
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
|
|
100
|
+
print(f"Found Shopify connection: {connection.get('name')} ({connection_id})")
|
|
101
|
+
if connection.get("shopDomain"):
|
|
102
|
+
print(f"Shop domain: {connection['shopDomain']}")
|
|
103
|
+
if connection.get("status"):
|
|
104
|
+
print(f"Stored status: {connection['status']}")
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
verified = verify_shopify_connection(client, connection_id)
|
|
108
|
+
except ConvexityError as exc:
|
|
109
|
+
print(f"Shopify verification failed: {exc}", file=sys.stderr)
|
|
110
|
+
print("Please reconnect or recreate the Shopify connection manually in Convexity, then rerun this sample.", file=sys.stderr)
|
|
111
|
+
sys.exit(1)
|
|
112
|
+
|
|
113
|
+
status = verified.get("status")
|
|
114
|
+
if status != "CONNECTED":
|
|
115
|
+
print(f"Shopify connection is not connected. Current status: {status or 'unknown'}", file=sys.stderr)
|
|
116
|
+
if verified.get("errorMessage"):
|
|
117
|
+
print(f"Error: {verified['errorMessage']}", file=sys.stderr)
|
|
118
|
+
print("Please reconnect or recreate the Shopify connection manually in Convexity, then rerun this sample.", file=sys.stderr)
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
|
|
121
|
+
print("Shopify connection is available and connected.")
|
|
122
|
+
if verified.get("shopName"):
|
|
123
|
+
print(f"Shop name: {verified['shopName']}")
|
|
124
|
+
if verified.get("grantedScopes"):
|
|
125
|
+
print("Granted scopes:")
|
|
126
|
+
for scope in verified["grantedScopes"]:
|
|
127
|
+
print(f" - {scope}")
|
|
128
|
+
finally:
|
|
129
|
+
client.close()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
main()
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Convexity SDK - Create Shopify Datasets
|
|
3
|
+
|
|
4
|
+
This sample creates Shopify-backed datasets from an existing Shopify connection,
|
|
5
|
+
refreshes them, and prints a small sanity check for row count and schema.
|
|
6
|
+
|
|
7
|
+
Prerequisites:
|
|
8
|
+
- Run 001_check_shopify_connection.py first.
|
|
9
|
+
- A valid API key with WRITE or ADMIN permission.
|
|
10
|
+
- A connected Shopify connection in the target project.
|
|
11
|
+
|
|
12
|
+
Optional environment variables:
|
|
13
|
+
- SHOPIFY_ORG_SLUG: organization slug, defaults to convexity-sdk-samples
|
|
14
|
+
- SHOPIFY_PROJECT_SLUG: project slug, defaults to griffin-it
|
|
15
|
+
- SHOPIFY_CONNECTION_NAME: when set, require this exact connection name
|
|
16
|
+
- SHOPIFY_DATASET_ENTITIES: comma-separated Shopify entities, defaults to all supported entities
|
|
17
|
+
- SHOPIFY_REFRESH_TIMEOUT_SECONDS: refresh timeout per dataset, defaults to 180
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
import time
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from convexity_api_client.errors import UnexpectedStatus
|
|
28
|
+
from convexity_api_client.models.dataset_config import DatasetConfig
|
|
29
|
+
from convexity_api_client.models.dataset_definition import DatasetDefinition
|
|
30
|
+
from convexity_api_client.models.dataset_type import DatasetType
|
|
31
|
+
from convexity_api_client.models.refresh_dataset_payload import RefreshDatasetPayload
|
|
32
|
+
from convexity_api_client.models.refresh_mode import RefreshMode
|
|
33
|
+
from convexity_api_client.models.shopify_dataset_config import ShopifyDatasetConfig
|
|
34
|
+
from convexity_api_client.models.shopify_entity import ShopifyEntity
|
|
35
|
+
from convexity_api_client.models.worker_task_status import WorkerTaskStatus
|
|
36
|
+
from convexity_api_client.types import Unset
|
|
37
|
+
|
|
38
|
+
from convexity_sdk import ConvexityClient
|
|
39
|
+
from convexity_sdk.exceptions import ConvexityError
|
|
40
|
+
|
|
41
|
+
ORG_SLUG = os.getenv("SHOPIFY_ORG_SLUG", "convexity-sdk-samples")
|
|
42
|
+
PROJECT_SLUG = os.getenv("SHOPIFY_PROJECT_SLUG", "griffin-it")
|
|
43
|
+
SHOPIFY_CONNECTION_NAME = os.getenv("SHOPIFY_CONNECTION_NAME")
|
|
44
|
+
DEFAULT_SHOPIFY_DATASET_ENTITIES = ",".join(entity.value for entity in ShopifyEntity)
|
|
45
|
+
SHOPIFY_DATASET_ENTITIES = os.getenv("SHOPIFY_DATASET_ENTITIES", DEFAULT_SHOPIFY_DATASET_ENTITIES)
|
|
46
|
+
REFRESH_TIMEOUT_SECONDS = int(os.getenv("SHOPIFY_REFRESH_TIMEOUT_SECONDS", "180"))
|
|
47
|
+
POLL_INTERVAL_SECONDS = 5
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_entities() -> list[ShopifyEntity]:
|
|
51
|
+
entities: list[ShopifyEntity] = []
|
|
52
|
+
for raw_entity in SHOPIFY_DATASET_ENTITIES.split(","):
|
|
53
|
+
entity_name = raw_entity.strip()
|
|
54
|
+
if not entity_name:
|
|
55
|
+
continue
|
|
56
|
+
try:
|
|
57
|
+
entities.append(ShopifyEntity(entity_name))
|
|
58
|
+
except ValueError:
|
|
59
|
+
valid = ", ".join(entity.value for entity in ShopifyEntity)
|
|
60
|
+
print(f"Unknown Shopify entity '{entity_name}'. Valid values: {valid}", file=sys.stderr)
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
if not entities:
|
|
63
|
+
print("SHOPIFY_DATASET_ENTITIES did not include any entities.", file=sys.stderr)
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
return entities
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def find_project_id(client: ConvexityClient) -> str:
|
|
69
|
+
org = client.organizations.get_by_slug(ORG_SLUG)
|
|
70
|
+
if org is None:
|
|
71
|
+
print(f"Organization '{ORG_SLUG}' not found. Run ../create_org_and_project.py first.", file=sys.stderr)
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
|
|
74
|
+
project = client.projects.get_by_slug(org.id, PROJECT_SLUG)
|
|
75
|
+
if project is None:
|
|
76
|
+
print(f"Project '{PROJECT_SLUG}' not found in organization '{ORG_SLUG}'.", file=sys.stderr)
|
|
77
|
+
sys.exit(1)
|
|
78
|
+
|
|
79
|
+
print(f"Using project: {project.name} ({project.id})")
|
|
80
|
+
return project.id
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def connection_to_dict(connection: Any) -> dict[str, Any]:
|
|
84
|
+
if hasattr(connection, "to_dict"):
|
|
85
|
+
data = connection.to_dict()
|
|
86
|
+
if isinstance(data, dict):
|
|
87
|
+
return data
|
|
88
|
+
return {}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def find_shopify_connection(client: ConvexityClient, project_id: str) -> dict[str, Any] | None:
|
|
92
|
+
connections = client.connections.list(project_id=project_id).connections
|
|
93
|
+
shopify_connections = []
|
|
94
|
+
for connection in connections:
|
|
95
|
+
data = connection_to_dict(connection)
|
|
96
|
+
if data.get("type") == "SHOPIFY":
|
|
97
|
+
shopify_connections.append(data)
|
|
98
|
+
|
|
99
|
+
if SHOPIFY_CONNECTION_NAME is not None:
|
|
100
|
+
return next((connection for connection in shopify_connections if connection.get("name") == SHOPIFY_CONNECTION_NAME), None)
|
|
101
|
+
|
|
102
|
+
return shopify_connections[0] if shopify_connections else None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def verify_shopify_connection(client: ConvexityClient, connection_id: str) -> dict[str, Any]:
|
|
106
|
+
response = client._http.request("post", f"/v1/connections/shopify/{connection_id}/verify")
|
|
107
|
+
data = response.json()
|
|
108
|
+
if not isinstance(data, dict):
|
|
109
|
+
raise ValueError(f"Unexpected Shopify verify response: {data!r}")
|
|
110
|
+
return data
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def dataset_name_for(entity: ShopifyEntity) -> str:
|
|
114
|
+
return f"shopify_{entity.value.lower()}"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def build_config(entity: ShopifyEntity) -> DatasetConfig:
|
|
118
|
+
return DatasetConfig(shopify=ShopifyDatasetConfig(entity=entity))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def find_existing_dataset(client: ConvexityClient, project_id: str, name: str) -> DatasetDefinition | None:
|
|
122
|
+
datasets = client.datasets.list(project_id).datasets
|
|
123
|
+
return next((dataset for dataset in datasets if dataset.name == name), None)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def create_or_update_dataset(
|
|
127
|
+
client: ConvexityClient,
|
|
128
|
+
*,
|
|
129
|
+
project_id: str,
|
|
130
|
+
connection_id: str,
|
|
131
|
+
entity: ShopifyEntity,
|
|
132
|
+
) -> DatasetDefinition:
|
|
133
|
+
name = dataset_name_for(entity)
|
|
134
|
+
config = build_config(entity)
|
|
135
|
+
existing = find_existing_dataset(client, project_id, name)
|
|
136
|
+
if existing is not None:
|
|
137
|
+
dataset = client.datasets.update(
|
|
138
|
+
existing.id,
|
|
139
|
+
connection_id=connection_id,
|
|
140
|
+
config=config,
|
|
141
|
+
refresh_mode=RefreshMode.MANUAL,
|
|
142
|
+
)
|
|
143
|
+
print(f"Updated dataset: {dataset.name} ({dataset.id})")
|
|
144
|
+
return dataset
|
|
145
|
+
|
|
146
|
+
dataset = client.datasets.create(
|
|
147
|
+
project_id=project_id,
|
|
148
|
+
name=name,
|
|
149
|
+
type_=DatasetType.SHOPIFY,
|
|
150
|
+
config=config,
|
|
151
|
+
connection_id=connection_id,
|
|
152
|
+
refresh_mode=RefreshMode.MANUAL,
|
|
153
|
+
description=f"Shopify {entity.value} dataset created by the SDK sample",
|
|
154
|
+
tags=["shopify", "sdk-sample"],
|
|
155
|
+
)
|
|
156
|
+
print(f"Created dataset: {dataset.name} ({dataset.id})")
|
|
157
|
+
return dataset
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def refresh_and_wait(client: ConvexityClient, dataset: DatasetDefinition) -> RefreshDatasetPayload:
|
|
161
|
+
try:
|
|
162
|
+
task = client.datasets.refresh(dataset.id)
|
|
163
|
+
except (UnexpectedStatus, ConvexityError, ValueError) as exc:
|
|
164
|
+
raise RuntimeError(f"Failed to trigger refresh for '{dataset.name}': {exc}") from exc
|
|
165
|
+
|
|
166
|
+
print(f" Refresh started (task_id={task.id})", end="", flush=True)
|
|
167
|
+
deadline = time.monotonic() + REFRESH_TIMEOUT_SECONDS
|
|
168
|
+
while True:
|
|
169
|
+
if time.monotonic() > deadline:
|
|
170
|
+
raise TimeoutError(f"Refresh timed out for '{dataset.name}' after {REFRESH_TIMEOUT_SECONDS} seconds")
|
|
171
|
+
|
|
172
|
+
time.sleep(POLL_INTERVAL_SECONDS)
|
|
173
|
+
state = client.datasets.get_refresh_status(dataset.id, task.id)
|
|
174
|
+
payload = state.payload
|
|
175
|
+
if isinstance(payload, RefreshDatasetPayload):
|
|
176
|
+
message = payload.progress_message if not isinstance(payload.progress_message, Unset) else None
|
|
177
|
+
if message:
|
|
178
|
+
print(f" {message}", end="", flush=True)
|
|
179
|
+
else:
|
|
180
|
+
payload = None
|
|
181
|
+
|
|
182
|
+
if state.status == WorkerTaskStatus.COMPLETED:
|
|
183
|
+
print(" COMPLETED")
|
|
184
|
+
if payload is None:
|
|
185
|
+
raise ValueError(f"Refresh completed for '{dataset.name}' without a refresh payload")
|
|
186
|
+
return payload
|
|
187
|
+
|
|
188
|
+
if state.status == WorkerTaskStatus.FAILED:
|
|
189
|
+
error = state.error if not isinstance(state.error, Unset) else "unknown error"
|
|
190
|
+
raise RuntimeError(f"Refresh failed for '{dataset.name}': {error}")
|
|
191
|
+
|
|
192
|
+
if state.status == WorkerTaskStatus.CANCELLED:
|
|
193
|
+
raise RuntimeError(f"Refresh was cancelled for '{dataset.name}'")
|
|
194
|
+
|
|
195
|
+
print(".", end="", flush=True)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def print_dataset_sanity(client: ConvexityClient, dataset: DatasetDefinition, payload: RefreshDatasetPayload) -> None:
|
|
199
|
+
refreshed = client.datasets.get(dataset.id)
|
|
200
|
+
if refreshed is None:
|
|
201
|
+
raise ValueError(f"Could not fetch refreshed dataset '{dataset.name}'")
|
|
202
|
+
|
|
203
|
+
row_count = payload.row_count if not isinstance(payload.row_count, Unset) else None
|
|
204
|
+
schema = refreshed.schema
|
|
205
|
+
columns = [] if schema is None or isinstance(schema, Unset) else schema
|
|
206
|
+
|
|
207
|
+
print(f" Rows written: {row_count if row_count is not None else 'unknown'}")
|
|
208
|
+
print(f" Columns: {len(columns)}")
|
|
209
|
+
for column in columns[:8]:
|
|
210
|
+
column_type = getattr(column, "type_", getattr(column, "type", "unknown"))
|
|
211
|
+
print(f" - {column.name}: {column_type}")
|
|
212
|
+
|
|
213
|
+
if row_count is None:
|
|
214
|
+
raise ValueError(f"Refresh for '{dataset.name}' did not report row_count")
|
|
215
|
+
if not columns:
|
|
216
|
+
raise ValueError(f"Refresh for '{dataset.name}' did not produce a schema")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def main() -> None:
|
|
220
|
+
entities = parse_entities()
|
|
221
|
+
client = ConvexityClient()
|
|
222
|
+
try:
|
|
223
|
+
print(f"Connected to {client.base_url}")
|
|
224
|
+
project_id = find_project_id(client)
|
|
225
|
+
|
|
226
|
+
connection = find_shopify_connection(client, project_id)
|
|
227
|
+
if connection is None:
|
|
228
|
+
print("Shopify connection is not available for this project.", file=sys.stderr)
|
|
229
|
+
print("Run 001_check_shopify_connection.py after creating the connection manually.", file=sys.stderr)
|
|
230
|
+
sys.exit(1)
|
|
231
|
+
|
|
232
|
+
connection_id = connection.get("id")
|
|
233
|
+
if not isinstance(connection_id, str) or not connection_id:
|
|
234
|
+
print(f"Shopify connection '{connection.get('name')}' has no connection ID.", file=sys.stderr)
|
|
235
|
+
sys.exit(1)
|
|
236
|
+
|
|
237
|
+
verified = verify_shopify_connection(client, connection_id)
|
|
238
|
+
if verified.get("status") != "CONNECTED":
|
|
239
|
+
print("Shopify connection is not connected. Run 001_check_shopify_connection.py first.", file=sys.stderr)
|
|
240
|
+
sys.exit(1)
|
|
241
|
+
|
|
242
|
+
print(f"Using Shopify connection: {connection.get('name')} ({connection_id})")
|
|
243
|
+
print("Entities:", ", ".join(entity.value for entity in entities))
|
|
244
|
+
|
|
245
|
+
for entity in entities:
|
|
246
|
+
print(f"\n-- {entity.value} --")
|
|
247
|
+
dataset = create_or_update_dataset(
|
|
248
|
+
client,
|
|
249
|
+
project_id=project_id,
|
|
250
|
+
connection_id=connection_id,
|
|
251
|
+
entity=entity,
|
|
252
|
+
)
|
|
253
|
+
payload = refresh_and_wait(client, dataset)
|
|
254
|
+
print_dataset_sanity(client, dataset, payload)
|
|
255
|
+
|
|
256
|
+
print("\nDone!")
|
|
257
|
+
finally:
|
|
258
|
+
client.close()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
if __name__ == "__main__":
|
|
262
|
+
main()
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
git pull# Convexity SDK Samples
|
|
2
|
-
|
|
3
|
-
This directory contains sample scripts demonstrating how to use the Convexity SDK.
|
|
4
|
-
|
|
5
|
-
## Samples
|
|
6
|
-
|
|
7
|
-
| Sample | Description |
|
|
8
|
-
| -------------------------------------------------------------------- | ------------------------------------------- |
|
|
9
|
-
| [getting_started.py](getting_started.py) | Basic SDK setup and usage |
|
|
10
|
-
| [create_org_and_project.py](create_org_and_project.py) | Create an organization and project |
|
|
11
|
-
| [create_databricks_connections.py](create_databricks_connections.py) | Create Databricks connections |
|
|
12
|
-
| [create_datasets.py](create_datasets.py) | Create datasets from Databricks connections |
|
|
13
|
-
| [create_dashboards.py](create_dashboards.py) | Create analytic dashboards with widgets |
|
|
14
|
-
| [share_dashboard.py](share_dashboard.py) | Share a dashboard with other users |
|
|
15
|
-
| [list_orgs_and_projects.py](list_orgs_and_projects.py) | List organizations and projects |
|
|
16
|
-
| [query_datalake.py](query_datalake.py) | Query the DuckLake warehouse with DuckDB |
|
|
17
|
-
| [cancel_refresh_task.py](cancel_refresh_task.py) | Cancel a pending dataset refresh task |
|
|
18
|
-
| [create_forecast_task.py](create_forecast_task.py) | Create a demand-forecasting task and run it |
|
|
19
|
-
| [compare_api_key_scopes.py](compare_api_key_scopes.py) | Compare READ vs ADMIN API key behavior |
|
|
20
|
-
| [debug_api_key_tenant_context.py](debug_api_key_tenant_context.py) | Diagnose API key tenant/scope mismatch |
|
|
21
|
-
|
|
22
|
-
## Running Samples
|
|
23
|
-
|
|
24
|
-
```bash
|
|
25
|
-
# From the repo root
|
|
26
|
-
uv run python packages/convexity-sdk/samples/getting_started.py
|
|
27
|
-
```
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Convexity SDK - Create Organization & Project
|
|
3
|
-
|
|
4
|
-
This sample demonstrates how to:
|
|
5
|
-
- Create a new organization
|
|
6
|
-
- Add members to the organization as admins
|
|
7
|
-
- Create a project within the organization
|
|
8
|
-
|
|
9
|
-
Prerequisites:
|
|
10
|
-
- A valid API key with WRITE or ADMIN permission
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from convexity_api_client.models.organization_role import OrganizationRole
|
|
14
|
-
|
|
15
|
-
from convexity_sdk import ConvexityClient
|
|
16
|
-
|
|
17
|
-
client = ConvexityClient()
|
|
18
|
-
|
|
19
|
-
# ── Step 1: Create a new organization ───────────────────────────────────
|
|
20
|
-
org = client.organizations.create(
|
|
21
|
-
name="Convexity SDK samples",
|
|
22
|
-
slug="convexity-sdk-samples",
|
|
23
|
-
description="Organization created by the Convexity SDK sample scripts",
|
|
24
|
-
)
|
|
25
|
-
print(f"Created organization: {org.name} (id={org.id})")
|
|
26
|
-
|
|
27
|
-
# ── Step 2: Add admin members ──────────────────────────────────────────
|
|
28
|
-
admin_user_ids = [
|
|
29
|
-
"6eadd2f4-08aa-4017-9131-6ba87d066b12",
|
|
30
|
-
"c572deb6-8c2e-4515-a23e-fb468a721048",
|
|
31
|
-
]
|
|
32
|
-
|
|
33
|
-
for user_id in admin_user_ids:
|
|
34
|
-
member = client.organizations.add_member(
|
|
35
|
-
org.id,
|
|
36
|
-
user_id=user_id,
|
|
37
|
-
role=OrganizationRole.ADMIN,
|
|
38
|
-
)
|
|
39
|
-
print(f" Added admin: {member.user_id} (role={member.role})")
|
|
40
|
-
|
|
41
|
-
# ── Step 3: Create a project in the organization ───────────────────────
|
|
42
|
-
project = client.projects.create(
|
|
43
|
-
name="griffin it",
|
|
44
|
-
slug="griffin-it",
|
|
45
|
-
organization_id=org.id,
|
|
46
|
-
description="Test project created by the Convexity SDK sample scripts",
|
|
47
|
-
)
|
|
48
|
-
print(f"Created project: {project.name} (id={project.id})")
|
|
49
|
-
|
|
50
|
-
print("\nDone!")
|
|
51
|
-
client.close()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/models/__init__.py
RENAMED
|
File without changes
|
{convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/__init__.py
RENAMED
|
File without changes
|
{convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/api_keys.py
RENAMED
|
File without changes
|
{convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/connections.py
RENAMED
|
File without changes
|
{convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/dashboards.py
RENAMED
|
File without changes
|
{convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/datalake.py
RENAMED
|
File without changes
|
{convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/datasets.py
RENAMED
|
File without changes
|
{convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/environments.py
RENAMED
|
File without changes
|
{convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/issues.py
RENAMED
|
File without changes
|
|
File without changes
|
{convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/tasks.py
RENAMED
|
File without changes
|
{convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/usage.py
RENAMED
|
File without changes
|
{convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/convexity_sdk/resources/workspace.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/compare_api_key_scopes.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/create_databricks_connections.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/create_single_project_api_key.py
RENAMED
|
File without changes
|
|
File without changes
|
{convexity_sdk-0.12.0.dev238 → convexity_sdk-0.12.0.dev239}/samples/list_orgs_and_projects.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|