poelis-sdk 0.1.4__py3-none-any.whl → 0.1.6__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.

Potentially problematic release.


This version of poelis-sdk might be problematic. Click here for more details.

poelis_sdk/__init__.py CHANGED
@@ -7,8 +7,9 @@ metadata so it stays in sync with ``pyproject.toml`` without manual edits.
7
7
  from importlib import metadata
8
8
 
9
9
  from .client import PoelisClient
10
+ from .logging import configure_logging, quiet_logging, verbose_logging, debug_logging, get_logger
10
11
 
11
- __all__ = ["PoelisClient", "__version__"]
12
+ __all__ = ["PoelisClient", "__version__", "configure_logging", "quiet_logging", "verbose_logging", "debug_logging", "get_logger"]
12
13
 
13
14
  def _resolve_version() -> str:
14
15
  """Return installed package version or a dev fallback.
poelis_sdk/browser.py CHANGED
@@ -3,6 +3,8 @@ from __future__ import annotations
3
3
  from typing import Any, Dict, List, Optional
4
4
  import re
5
5
 
6
+ from .org_validation import get_organization_context_message
7
+
6
8
  """GraphQL-backed dot-path browser for Poelis SDK.
7
9
 
8
10
  Provides lazy, name-based navigation across workspaces → products → items → child items,
@@ -204,7 +206,9 @@ class Browser:
204
206
  return getattr(self._root, attr)
205
207
 
206
208
  def __repr__(self) -> str: # pragma: no cover - notebook UX
207
- return "<browser root>"
209
+ org_id = self._root._client.org_id
210
+ org_context = get_organization_context_message(org_id) if org_id else "🔒 Organization: Not configured"
211
+ return f"<browser root> ({org_context})"
208
212
 
209
213
  def __getitem__(self, key: str) -> Any: # pragma: no cover - notebook UX
210
214
  """Delegate index-based access to the root node so names work: browser["Workspace Name"]."""
poelis_sdk/client.py CHANGED
@@ -10,6 +10,7 @@ from .items import ItemsClient
10
10
  from .search import SearchClient
11
11
  from .workspaces import WorkspacesClient
12
12
  from .browser import Browser
13
+ from .logging import quiet_logging
13
14
 
14
15
  """Core client for the Poelis Python SDK.
15
16
 
@@ -54,6 +55,9 @@ class PoelisClient:
54
55
  timeout_seconds: Network timeout in seconds.
55
56
  """
56
57
 
58
+ # Configure quiet logging by default for production use
59
+ quiet_logging()
60
+
57
61
  self._config = ClientConfig(
58
62
  base_url=base_url,
59
63
  api_key=api_key,
@@ -70,10 +74,10 @@ class PoelisClient:
70
74
  )
71
75
 
72
76
  # Resource clients
73
- self.products = ProductsClient(self._transport)
77
+ self.workspaces = WorkspacesClient(self._transport)
78
+ self.products = ProductsClient(self._transport, self.workspaces)
74
79
  self.items = ItemsClient(self._transport)
75
80
  self.search = SearchClient(self._transport)
76
- self.workspaces = WorkspacesClient(self._transport)
77
81
  self.browser = Browser(self)
78
82
 
79
83
  @classmethod
poelis_sdk/items.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from typing import Generator, Any, Optional, Dict, List
4
4
 
5
5
  from ._transport import Transport
6
+ from .org_validation import validate_item_organization, filter_by_organization
6
7
 
7
8
  """Items resource client."""
8
9
 
@@ -14,11 +15,14 @@ class ItemsClient:
14
15
  self._t = transport
15
16
 
16
17
  def list_by_product(self, *, product_id: str, q: Optional[str] = None, limit: int = 100, offset: int = 0) -> List[Dict[str, Any]]:
17
- """List items for a product via GraphQL with optional text filter."""
18
+ """List items for a product via GraphQL with optional text filter.
19
+
20
+ Returns only items that belong to the client's configured organization.
21
+ """
18
22
 
19
23
  query = (
20
24
  "query($pid: ID!, $q: String, $limit: Int!, $offset: Int!) {\n"
21
- " items(productId: $pid, q: $q, limit: $limit, offset: $offset) { id name code description productId parentId owner }\n"
25
+ " items(productId: $pid, q: $q, limit: $limit, offset: $offset) { id name code description productId parentId owner orgId }\n"
22
26
  "}"
23
27
  )
24
28
  variables = {"pid": product_id, "q": q, "limit": int(limit), "offset": int(offset)}
@@ -27,14 +31,24 @@ class ItemsClient:
27
31
  payload = resp.json()
28
32
  if "errors" in payload:
29
33
  raise RuntimeError(str(payload["errors"]))
30
- return payload.get("data", {}).get("items", [])
34
+
35
+ items = payload.get("data", {}).get("items", [])
36
+
37
+ # Client-side organization filtering as backup protection
38
+ expected_org_id = self._t._org_id
39
+ filtered_items = filter_by_organization(items, expected_org_id, "items")
40
+
41
+ return filtered_items
31
42
 
32
43
  def get(self, item_id: str) -> Dict[str, Any]:
33
- """Get a single item by id via GraphQL."""
44
+ """Get a single item by id via GraphQL.
45
+
46
+ Returns the item only if it belongs to the client's configured organization.
47
+ """
34
48
 
35
49
  query = (
36
50
  "query($id: ID!) {\n"
37
- " item(id: $id) { id name code description productId parentId owner }\n"
51
+ " item(id: $id) { id name code description productId parentId owner orgId }\n"
38
52
  "}"
39
53
  )
40
54
  resp = self._t.graphql(query=query, variables={"id": item_id})
@@ -42,7 +56,16 @@ class ItemsClient:
42
56
  payload = resp.json()
43
57
  if "errors" in payload:
44
58
  raise RuntimeError(str(payload["errors"]))
45
- return payload.get("data", {}).get("item")
59
+
60
+ item = payload.get("data", {}).get("item")
61
+ if item is None:
62
+ raise RuntimeError(f"Item with id '{item_id}' not found")
63
+
64
+ # Validate that the item belongs to the configured organization
65
+ expected_org_id = self._t._org_id
66
+ validate_item_organization(item, expected_org_id)
67
+
68
+ return item
46
69
 
47
70
  def iter_all_by_product(self, *, product_id: str, q: Optional[str] = None, page_size: int = 100) -> Generator[dict, None, None]:
48
71
  """Iterate items via GraphQL for a given product."""
poelis_sdk/logging.py ADDED
@@ -0,0 +1,73 @@
1
+ """Logging configuration for the Poelis SDK.
2
+
3
+ This module provides utilities to configure logging levels for the SDK and its dependencies.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+
10
+
11
+ def configure_logging(
12
+ level: str = "WARNING",
13
+ disable_httpx_logs: bool = True,
14
+ disable_urllib3_logs: bool = True,
15
+ enable_sdk_logs: bool = False,
16
+ ) -> None:
17
+ """Configure logging for the Poelis SDK and its dependencies.
18
+
19
+ Args:
20
+ level: Logging level for the root logger (DEBUG, INFO, WARNING, ERROR, CRITICAL).
21
+ disable_httpx_logs: Whether to disable httpx HTTP request logs.
22
+ disable_urllib3_logs: Whether to disable urllib3 logs.
23
+ enable_sdk_logs: Whether to enable SDK-specific debug logs.
24
+ """
25
+ # Set root logger level
26
+ numeric_level = getattr(logging, level.upper(), logging.WARNING)
27
+ logging.basicConfig(level=numeric_level)
28
+
29
+ # Configure httpx logging
30
+ if disable_httpx_logs:
31
+ logging.getLogger("httpx").setLevel(logging.WARNING)
32
+ else:
33
+ logging.getLogger("httpx").setLevel(logging.INFO)
34
+
35
+ # Configure urllib3 logging
36
+ if disable_urllib3_logs:
37
+ logging.getLogger("urllib3").setLevel(logging.WARNING)
38
+ logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)
39
+
40
+ # Configure SDK logging
41
+ sdk_logger = logging.getLogger("poelis_sdk")
42
+ if enable_sdk_logs:
43
+ sdk_logger.setLevel(logging.DEBUG)
44
+ else:
45
+ sdk_logger.setLevel(logging.WARNING)
46
+
47
+
48
+ def get_logger(name: str) -> logging.Logger:
49
+ """Get a logger for the given name.
50
+
51
+ Args:
52
+ name: Logger name, typically __name__.
53
+
54
+ Returns:
55
+ Logger instance.
56
+ """
57
+ return logging.getLogger(f"poelis_sdk.{name}")
58
+
59
+
60
+ # Convenience functions for common logging configurations
61
+ def quiet_logging() -> None:
62
+ """Configure quiet logging - only show warnings and errors."""
63
+ configure_logging(level="WARNING", disable_httpx_logs=True, disable_urllib3_logs=True)
64
+
65
+
66
+ def verbose_logging() -> None:
67
+ """Configure verbose logging - show all logs including HTTP requests."""
68
+ configure_logging(level="INFO", disable_httpx_logs=False, disable_urllib3_logs=False, enable_sdk_logs=True)
69
+
70
+
71
+ def debug_logging() -> None:
72
+ """Configure debug logging - show everything including SDK debug logs."""
73
+ configure_logging(level="DEBUG", disable_httpx_logs=False, disable_urllib3_logs=False, enable_sdk_logs=True)
@@ -0,0 +1,161 @@
1
+ """Organization validation utilities for the Poelis SDK.
2
+
3
+ This module provides utilities for validating that data belongs to the
4
+ configured organization, ensuring proper multi-tenant isolation.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ from .exceptions import ClientError
12
+
13
+
14
+ class OrganizationValidationError(ClientError):
15
+ """Raised when data doesn't belong to the configured organization."""
16
+
17
+ def __init__(self, message: str, expected_org_id: str, actual_org_id: Optional[str] = None) -> None:
18
+ """Initialize organization validation error.
19
+
20
+ Args:
21
+ message: Error message describing the validation failure.
22
+ expected_org_id: The organization ID that was expected.
23
+ actual_org_id: The organization ID that was found (if any).
24
+ """
25
+ super().__init__(400, message)
26
+ self.expected_org_id = expected_org_id
27
+ self.actual_org_id = actual_org_id
28
+
29
+
30
+ def validate_organization_id(data: Dict[str, Any], expected_org_id: str, data_type: str = "item") -> None:
31
+ """Validate that data belongs to the expected organization.
32
+
33
+ Args:
34
+ data: The data dictionary to validate.
35
+ expected_org_id: The organization ID that should match.
36
+ data_type: Type of data being validated (for error messages).
37
+
38
+ Raises:
39
+ OrganizationValidationError: If the data doesn't belong to the expected organization.
40
+ """
41
+ actual_org_id = data.get('orgId')
42
+
43
+ if actual_org_id is None:
44
+ raise OrganizationValidationError(
45
+ f"{data_type.capitalize()} does not have an organization ID",
46
+ expected_org_id,
47
+ actual_org_id
48
+ )
49
+
50
+ if actual_org_id != expected_org_id:
51
+ raise OrganizationValidationError(
52
+ f"{data_type.capitalize()} belongs to organization '{actual_org_id}', "
53
+ f"but client is configured for organization '{expected_org_id}'",
54
+ expected_org_id,
55
+ actual_org_id
56
+ )
57
+
58
+
59
+ def filter_by_organization(data_list: List[Dict[str, Any]], expected_org_id: str, data_type: str = "items") -> List[Dict[str, Any]]:
60
+ """Filter a list of data to only include items from the expected organization.
61
+
62
+ Args:
63
+ data_list: List of data dictionaries to filter.
64
+ expected_org_id: The organization ID to filter by.
65
+ data_type: Type of data being filtered (for logging).
66
+
67
+ Returns:
68
+ Filtered list containing only data from the expected organization.
69
+ """
70
+ filtered = []
71
+ cross_org_count = 0
72
+
73
+ for item in data_list:
74
+ item_org_id = item.get('orgId')
75
+ if item_org_id == expected_org_id:
76
+ filtered.append(item)
77
+ else:
78
+ cross_org_count += 1
79
+
80
+ if cross_org_count > 0:
81
+ # Log warning about cross-org data (but don't fail)
82
+ print(f"⚠️ Warning: Filtered out {cross_org_count} {data_type} from other organizations")
83
+
84
+ return filtered
85
+
86
+
87
+ def validate_workspace_organization(workspace: Dict[str, Any], expected_org_id: str) -> None:
88
+ """Validate that a workspace belongs to the expected organization.
89
+
90
+ Args:
91
+ workspace: The workspace dictionary to validate.
92
+ expected_org_id: The organization ID that should match.
93
+
94
+ Raises:
95
+ OrganizationValidationError: If the workspace doesn't belong to the expected organization.
96
+ """
97
+ validate_organization_id(workspace, expected_org_id, "workspace")
98
+
99
+
100
+ def validate_product_organization(product: Any, expected_org_id: str) -> None:
101
+ """Validate that a product belongs to the expected organization.
102
+
103
+ Args:
104
+ product: The product object to validate (can be dict or Product model).
105
+ expected_org_id: The organization ID that should match.
106
+
107
+ Raises:
108
+ OrganizationValidationError: If the product doesn't belong to the expected organization.
109
+ """
110
+ # Handle both dict and Product model
111
+ if hasattr(product, 'workspace_id'):
112
+ # Product model - we need to get the workspace to check its org
113
+ # This is a limitation of the current API design
114
+ pass # We'll handle this in the client methods
115
+ elif isinstance(product, dict):
116
+ # Dict format - check if it has orgId directly
117
+ if 'orgId' in product:
118
+ validate_organization_id(product, expected_org_id, "product")
119
+ # If no orgId, we can't validate (backend should handle this)
120
+
121
+
122
+ def validate_item_organization(item: Dict[str, Any], expected_org_id: str) -> None:
123
+ """Validate that an item belongs to the expected organization.
124
+
125
+ Args:
126
+ item: The item dictionary to validate.
127
+ expected_org_id: The organization ID that should match.
128
+
129
+ Raises:
130
+ OrganizationValidationError: If the item doesn't belong to the expected organization.
131
+ """
132
+ validate_organization_id(item, expected_org_id, "item")
133
+
134
+
135
+ def get_organization_context_message(org_id: str) -> str:
136
+ """Get a user-friendly message about the current organization context.
137
+
138
+ Args:
139
+ org_id: The current organization ID.
140
+
141
+ Returns:
142
+ A formatted message about the organization context.
143
+ """
144
+ return f"🔒 Organization: {org_id}"
145
+
146
+
147
+ def format_organization_error(error: OrganizationValidationError) -> str:
148
+ """Format an organization validation error for user display.
149
+
150
+ Args:
151
+ error: The organization validation error.
152
+
153
+ Returns:
154
+ A formatted error message.
155
+ """
156
+ return (
157
+ f"❌ Organization Mismatch: {error.message}\n"
158
+ f" Expected: {error.expected_org_id}\n"
159
+ f" Found: {error.actual_org_id or 'None'}\n"
160
+ f" This usually means the data belongs to a different organization."
161
+ )
poelis_sdk/products.py CHANGED
@@ -1,20 +1,24 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Generator, Optional, List
3
+ from typing import Generator, Optional, List, TYPE_CHECKING
4
4
 
5
5
  from ._transport import Transport
6
6
  from .models import PaginatedProducts, Product
7
7
 
8
+ if TYPE_CHECKING:
9
+ from .workspaces import WorkspacesClient
10
+
8
11
  """Products resource client."""
9
12
 
10
13
 
11
14
  class ProductsClient:
12
15
  """Client for product resources."""
13
16
 
14
- def __init__(self, transport: Transport) -> None:
15
- """Initialize with shared transport."""
17
+ def __init__(self, transport: Transport, workspaces_client: Optional["WorkspacesClient"] = None) -> None:
18
+ """Initialize with shared transport and optional workspaces client."""
16
19
 
17
20
  self._t = transport
21
+ self._workspaces_client = workspaces_client
18
22
 
19
23
  def list_by_workspace(self, *, workspace_id: str, q: Optional[str] = None, limit: int = 100, offset: int = 0) -> PaginatedProducts:
20
24
  """List products using GraphQL for a given workspace.
@@ -52,4 +56,26 @@ class ProductsClient:
52
56
  yield product
53
57
  offset += len(page.data)
54
58
 
59
+ def iter_all(self, *, q: Optional[str] = None, page_size: int = 100) -> Generator[Product, None, None]:
60
+ """Iterate products across all workspaces.
61
+
62
+ Args:
63
+ q: Optional free-text filter.
64
+ page_size: Page size for each workspace iteration.
65
+
66
+ Raises:
67
+ RuntimeError: If workspaces client is not available.
68
+ """
69
+ if self._workspaces_client is None:
70
+ raise RuntimeError("Workspaces client not available. Cannot iterate across all workspaces.")
71
+
72
+ # Get all workspaces
73
+ workspaces = self._workspaces_client.list(limit=1000, offset=0)
74
+
75
+ for workspace in workspaces:
76
+ workspace_id = workspace['id']
77
+ # Iterate through products in this workspace
78
+ for product in self.iter_all_by_workspace(workspace_id=workspace_id, q=q, page_size=page_size):
79
+ yield product
80
+
55
81
 
poelis_sdk/search.py CHANGED
@@ -54,7 +54,7 @@ class SearchClient:
54
54
  "query($q: String!, $ws: ID, $pid: ID, $iid: ID, $ptype: String, $cat: String, $limit: Int!, $offset: Int!, $sort: String) {\n"
55
55
  " searchProperties(q: $q, workspaceId: $ws, productId: $pid, itemId: $iid, propertyType: $ptype, category: $cat, limit: $limit, offset: $offset, sort: $sort) {\n"
56
56
  " query total limit offset processingTimeMs\n"
57
- " hits { id workspaceId productId itemId propertyType name category numericValue textValue dateValue owner }\n"
57
+ " hits { id workspaceId productId itemId propertyType name category value owner }\n"
58
58
  " }\n"
59
59
  "}"
60
60
  )
poelis_sdk/workspaces.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from typing import Any, Dict, List, Optional
4
4
 
5
5
  from ._transport import Transport
6
+ from .org_validation import validate_workspace_organization, filter_by_organization
6
7
 
7
8
  """Workspaces GraphQL client."""
8
9
 
@@ -14,7 +15,10 @@ class WorkspacesClient:
14
15
  self._t = transport
15
16
 
16
17
  def list(self, *, limit: int = 50, offset: int = 0) -> List[Dict[str, Any]]:
17
- """List workspaces (implicitly scoped by org via auth)."""
18
+ """List workspaces (implicitly scoped by org via auth).
19
+
20
+ Returns only workspaces that belong to the client's configured organization.
21
+ """
18
22
 
19
23
  query = (
20
24
  "query($limit: Int!, $offset: Int!) {\n"
@@ -26,10 +30,20 @@ class WorkspacesClient:
26
30
  payload = resp.json()
27
31
  if "errors" in payload:
28
32
  raise RuntimeError(str(payload["errors"]))
29
- return payload.get("data", {}).get("workspaces", [])
33
+
34
+ workspaces = payload.get("data", {}).get("workspaces", [])
35
+
36
+ # Client-side organization filtering as backup protection
37
+ expected_org_id = self._t._org_id
38
+ filtered_workspaces = filter_by_organization(workspaces, expected_org_id, "workspaces")
39
+
40
+ return filtered_workspaces
30
41
 
31
42
  def get(self, *, workspace_id: str) -> Optional[Dict[str, Any]]:
32
- """Get a single workspace by id via GraphQL."""
43
+ """Get a single workspace by id via GraphQL.
44
+
45
+ Returns the workspace only if it belongs to the client's configured organization.
46
+ """
33
47
 
34
48
  query = (
35
49
  "query($id: ID!) {\n"
@@ -41,6 +55,15 @@ class WorkspacesClient:
41
55
  payload = resp.json()
42
56
  if "errors" in payload:
43
57
  raise RuntimeError(str(payload["errors"]))
44
- return payload.get("data", {}).get("workspace")
58
+
59
+ workspace = payload.get("data", {}).get("workspace")
60
+ if workspace is None:
61
+ return None
62
+
63
+ # Validate that the workspace belongs to the configured organization
64
+ expected_org_id = self._t._org_id
65
+ validate_workspace_organization(workspace, expected_org_id)
66
+
67
+ return workspace
45
68
 
46
69
 
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: poelis-sdk
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: Official Python SDK for Poelis
5
- Project-URL: Homepage, https://poelis.ai
5
+ Project-URL: Homepage, https://poelis.com
6
6
  Project-URL: Source, https://github.com/PoelisTechnologies/poelis-python-sdk
7
7
  Project-URL: Issues, https://github.com/PoelisTechnologies/poelis-python-sdk/issues
8
8
  Author-email: Matteo Braceschi <matteo@poelis.com>
@@ -23,7 +23,15 @@ Description-Content-Type: text/markdown
23
23
 
24
24
  # Poelis Python SDK
25
25
 
26
- GraphQL-first Python SDK for Poelis with a focus on great developer experience.
26
+ Python SDK for Poelis.
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ pip install -U poelis-sdk
32
+ ```
33
+
34
+ Requires Python 3.11+.
27
35
 
28
36
  ## Quickstart (API key + org ID)
29
37
 
@@ -33,40 +41,27 @@ from poelis_sdk import PoelisClient
33
41
  client = PoelisClient(
34
42
  api_key="poelis_live_A1B2C3...", # Organization Settings → API Keys
35
43
  org_id="tenant_uci_001", # same section
36
- # base_url defaults to https://api.poelis.ai
37
44
  )
38
45
 
39
- # Workspaces → Products (GraphQL)
46
+ # Workspaces → Products
40
47
  workspaces = client.workspaces.list(limit=10, offset=0)
41
48
  ws_id = workspaces[0]["id"]
42
49
 
43
50
  page = client.products.list_by_workspace(workspace_id=ws_id, limit=10, offset=0)
44
51
  print([p.name for p in page.data])
45
52
 
46
- # Items for a product (GraphQL)
53
+ # Items for a product
47
54
  pid = page.data[0].id
48
55
  items = client.items.list_by_product(product_id=pid, limit=10, offset=0)
49
56
  print([i.get("name") for i in items])
50
57
 
51
- # Property search (GraphQL)
58
+ # Property search
52
59
  props = client.search.properties(q="*", workspace_id=ws_id, limit=10, offset=0)
53
60
  print(props["total"], len(props["hits"]))
54
61
  ```
55
62
 
56
63
  ## Configuration
57
64
 
58
- ### Base URL
59
-
60
- The SDK defaults to the production API (`https://api.poelis.ai`). You can override this for different environments:
61
-
62
- - Local development: `base_url="http://localhost:8000"`
63
- - Staging (example): `base_url="https://api.staging.poelis.ai"`
64
- - Production (default): No need to specify, uses `https://api.poelis.ai`
65
-
66
- Confirm the exact URLs for your environments.
67
-
68
- Note: Multi-tenancy uses `org_id` for scoping. When using API keys, the SDK sets `X-Poelis-Org` automatically from `org_id`.
69
-
70
65
  ### Getting your API key and org ID
71
66
 
72
67
  1. Navigate to Organization Settings → API Keys.
@@ -77,8 +72,8 @@ Note: Multi-tenancy uses `org_id` for scoping. When using API keys, the SDK sets
77
72
 
78
73
  ```bash
79
74
  export POELIS_API_KEY=poelis_live_A1B2C3...
80
- export POELIS_ORG_ID=tenant_uci_001
81
- # POELIS_BASE_URL is optional - defaults to https://api.poelis.ai
75
+ export POELIS_ORG_ID=tenant_id_001
76
+ # POELIS_BASE_URL is optional - defaults to the managed GCP endpoint
82
77
  ```
83
78
 
84
79
 
@@ -91,8 +86,7 @@ client.browser # then use TAB to explore
91
86
  # client.browser.<workspace>.<product>.<item>.<child>.properties
92
87
  ```
93
88
 
94
- - Lazy-loaded via GraphQL on-demand.
95
- - Autocomplete-friendly in Jupyter/VSCode.
89
+ See the example notebook in `notebooks/try_poelis_sdk.ipynb` for an end-to-end walkthrough (authentication, listing workspaces/products/items, and simple search queries).
96
90
 
97
91
  ## Requirements
98
92
 
@@ -101,4 +95,4 @@ client.browser # then use TAB to explore
101
95
 
102
96
  ## License
103
97
 
104
- Apache-2.0
98
+ MIT
@@ -0,0 +1,17 @@
1
+ poelis_sdk/__init__.py,sha256=vRKuvnMGtq2_6SYDPNpckSPYXTgMDD1vBAfZ1bXlHL0,924
2
+ poelis_sdk/_transport.py,sha256=F5EX0EJFHJPAE638nKzlX5zLSU6FIMzMemqgh05_V6U,6061
3
+ poelis_sdk/auth0.py,sha256=VDZHCv9YpsW55H-PLINKMq74UhevP6OWyBHQyEFIpvw,3163
4
+ poelis_sdk/browser.py,sha256=i3j2G2eDCD8JPPtRnU-z_AOup0aBGIKisilVmJuqb-w,12645
5
+ poelis_sdk/client.py,sha256=10__5po-foX36ZCCduQmzdoh9NNS320kyaqztUNtPvo,3872
6
+ poelis_sdk/exceptions.py,sha256=qX5kpAr8ozJUOW-CNhmspWVIE-bvUZT_PUnimYuBxNY,1101
7
+ poelis_sdk/items.py,sha256=laRHVCaTkMmNQyXs6_IVrsyj6whMxSN8Qi6aPQ4dN00,3036
8
+ poelis_sdk/logging.py,sha256=zmg8Us-7qjDl0n_NfOSvDolLopy7Dc_hQ-pcrC63dY8,2442
9
+ poelis_sdk/models.py,sha256=zKbqHkK2xOdkqWUQlmu-BZ0Zyj8uC2d10PK69f3QUHo,470
10
+ poelis_sdk/org_validation.py,sha256=H0jyer8qEAttIW-WC0sS4_32GoC8fXB0rjbn1_rZwQ0,5803
11
+ poelis_sdk/products.py,sha256=XEi028Gqfpp887gBgvSA5Punzae4AfaorwkzuLwX890,3232
12
+ poelis_sdk/search.py,sha256=JYLz4yV3GZPlif05OqYK2xPoAD1b4XKTmriil4NHTOs,4095
13
+ poelis_sdk/workspaces.py,sha256=hpmRl-Hswr4YDvObQdyVpegIYjUWno7A_BiVBz-AQGc,2383
14
+ poelis_sdk-0.1.6.dist-info/METADATA,sha256=xcvgrU6sH4VGqsceIf4Q0V-5-0IKbFOmMfVo_OsLbrM,2813
15
+ poelis_sdk-0.1.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
+ poelis_sdk-0.1.6.dist-info/licenses/LICENSE,sha256=EEmE_r8wk_pdXB8CWp1LG6sBOl7--hNSS2kV94cI6co,1075
17
+ poelis_sdk-0.1.6.dist-info/RECORD,,
@@ -1,34 +0,0 @@
1
- name: SDK CI
2
-
3
- on:
4
- push:
5
- paths:
6
- - 'sdk/python/**'
7
- pull_request:
8
- paths:
9
- - 'sdk/python/**'
10
-
11
- jobs:
12
- build:
13
- runs-on: ubuntu-latest
14
- defaults:
15
- run:
16
- working-directory: sdk/python
17
- steps:
18
- - uses: actions/checkout@v4
19
- - name: Set up Python
20
- uses: actions/setup-python@v5
21
- with:
22
- python-version: '3.11'
23
- - name: Install uv
24
- run: curl -LsSf https://astral.sh/uv/install.sh | sh
25
- - name: Install deps
26
- run: |
27
- ~/.local/bin/uv pip install -e .
28
- ~/.local/bin/uv pip install pytest ruff
29
- - name: Lint (ruff)
30
- run: ~/.local/bin/uv run ruff check src tests
31
- - name: Test (pytest)
32
- run: ~/.local/bin/uv run pytest -q
33
-
34
-
@@ -1,55 +0,0 @@
1
- name: SDK Docs
2
-
3
- on:
4
- push:
5
- branches: [ main ]
6
- paths:
7
- - 'sdk/python/docs/**'
8
- - 'sdk/python/mkdocs.yml'
9
- - 'sdk/python/src/**'
10
-
11
- permissions:
12
- contents: read
13
- pages: write
14
- id-token: write
15
-
16
- concurrency:
17
- group: 'pages'
18
- cancel-in-progress: true
19
-
20
- jobs:
21
- build:
22
- runs-on: ubuntu-latest
23
- defaults:
24
- run:
25
- working-directory: sdk/python
26
- steps:
27
- - uses: actions/checkout@v4
28
- - uses: actions/setup-python@v5
29
- with:
30
- python-version: '3.11'
31
- - name: Install uv
32
- run: curl -LsSf https://astral.sh/uv/install.sh | sh
33
- - name: Install docs deps
34
- run: |
35
- ~/.local/bin/uv pip install -e .[docs]
36
- - name: Build docs
37
- run: |
38
- ~/.local/bin/uv run mkdocs build --strict
39
- - name: Upload artifact
40
- uses: actions/upload-pages-artifact@v3
41
- with:
42
- path: sdk/python/site
43
-
44
- deploy:
45
- needs: build
46
- runs-on: ubuntu-latest
47
- environment:
48
- name: github-pages
49
- url: ${{ steps.deployment.outputs.page_url }}
50
- steps:
51
- - name: Deploy to GitHub Pages
52
- id: deployment
53
- uses: actions/deploy-pages@v4
54
-
55
-
@@ -1,31 +0,0 @@
1
- name: SDK Publish (TestPyPI)
2
-
3
- on:
4
- push:
5
- tags:
6
- - 'sdk-v*'
7
-
8
- jobs:
9
- build-and-publish:
10
- runs-on: ubuntu-latest
11
- defaults:
12
- run:
13
- working-directory: sdk/python
14
- steps:
15
- - uses: actions/checkout@v4
16
- - uses: actions/setup-python@v5
17
- with:
18
- python-version: '3.11'
19
- - name: Install uv
20
- run: curl -LsSf https://astral.sh/uv/install.sh | sh
21
- - name: Build wheel and sdist
22
- run: ~/.local/bin/uv build
23
- - name: Publish to TestPyPI
24
- env:
25
- TWINE_USERNAME: __token__
26
- TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }}
27
- run: |
28
- ~/.local/bin/uv pip install twine
29
- ~/.local/bin/uv run twine upload --repository-url https://test.pypi.org/legacy/ dist/*
30
-
31
-
@@ -1,18 +0,0 @@
1
- poelis_sdk/__init__.py,sha256=QzPuy0lsmF0JEOrcnHDWsUfR_WCO1XpegwrYpSGyUVI,738
2
- poelis_sdk/_transport.py,sha256=F5EX0EJFHJPAE638nKzlX5zLSU6FIMzMemqgh05_V6U,6061
3
- poelis_sdk/auth0.py,sha256=VDZHCv9YpsW55H-PLINKMq74UhevP6OWyBHQyEFIpvw,3163
4
- poelis_sdk/browser.py,sha256=zyMoNqFCvKZDV4ZSFVk2N1-HHiq80gzmkGN3uRo1CuM,12409
5
- poelis_sdk/client.py,sha256=Sr05go8eNpEXswWAhomQuSakE5Oai_kUsGDwHgPnnLY,3731
6
- poelis_sdk/exceptions.py,sha256=qX5kpAr8ozJUOW-CNhmspWVIE-bvUZT_PUnimYuBxNY,1101
7
- poelis_sdk/items.py,sha256=uFm-fu16QUOsVnlnEDF012zpgvySlN9N0SXMwIWXeOw,2183
8
- poelis_sdk/models.py,sha256=zKbqHkK2xOdkqWUQlmu-BZ0Zyj8uC2d10PK69f3QUHo,470
9
- poelis_sdk/products.py,sha256=Byc5XBNruIO-vAGxDom0lRWA3xxD6korMhoajPz83R4,2073
10
- poelis_sdk/search.py,sha256=YxtjR8AD6tOeVNTVWc8J_l9YkVu2f-EnC_udkMAeFiM,4122
11
- poelis_sdk/workspaces.py,sha256=LNVt73nqdssNx42_YB_V5Qp35kEdFn9rNBYmEjpM7vk,1518
12
- poelis_sdk/.github/workflows/sdk-ci.yml,sha256=hWO-igHeTAsxEJGCueteEQnAEi00GWXJJPa8DWgqhHM,750
13
- poelis_sdk/.github/workflows/sdk-docs.yml,sha256=bS1uUxOKRMA6TWrmzzJHTokyP0Nt0aJwojcLAgLoEhs,1166
14
- poelis_sdk/.github/workflows/sdk-publish-testpypi.yml,sha256=FBZcfDrtUijs6rcC8WeIimi9SfgoB8Xm5pTNtcztT44,776
15
- poelis_sdk-0.1.4.dist-info/METADATA,sha256=5vzMFlh7cIRQggWa8fRyoC1ao9V-JsP-C2_XgrFx2uM,3280
16
- poelis_sdk-0.1.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
17
- poelis_sdk-0.1.4.dist-info/licenses/LICENSE,sha256=EEmE_r8wk_pdXB8CWp1LG6sBOl7--hNSS2kV94cI6co,1075
18
- poelis_sdk-0.1.4.dist-info/RECORD,,