poelis-sdk 0.3.2__tar.gz → 0.3.4__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.

Potentially problematic release.


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

Files changed (32) hide show
  1. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/PKG-INFO +1 -1
  2. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/pyproject.toml +1 -1
  3. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/src/poelis_sdk/__init__.py +1 -1
  4. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/src/poelis_sdk/_transport.py +11 -3
  5. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/src/poelis_sdk/browser.py +62 -20
  6. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/src/poelis_sdk/client.py +5 -4
  7. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/src/poelis_sdk/items.py +1 -1
  8. poelis_sdk-0.3.4/src/poelis_sdk/models.py +130 -0
  9. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/src/poelis_sdk/products.py +1 -1
  10. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/src/poelis_sdk/search.py +1 -1
  11. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/src/poelis_sdk/workspaces.py +1 -1
  12. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/tests/test_integration_smoke.py +1 -0
  13. poelis_sdk-0.3.4/tests/test_typed_properties.py +212 -0
  14. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/uv.lock +1 -1
  15. poelis_sdk-0.3.2/src/poelis_sdk/models.py +0 -27
  16. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/.github/workflows/ci.yml +0 -0
  17. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/.github/workflows/codeql.yml +0 -0
  18. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/.github/workflows/publish-on-push.yml +0 -0
  19. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/.gitignore +0 -0
  20. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/LICENSE +0 -0
  21. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/README.md +0 -0
  22. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/notebooks/try_poelis_sdk.ipynb +0 -0
  23. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/src/poelis_sdk/exceptions.py +0 -0
  24. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/src/poelis_sdk/logging.py +0 -0
  25. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/src/poelis_sdk/org_validation.py +0 -0
  26. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/src/tests/test_client_basic.py +1 -1
  27. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/src/tests/test_errors_and_backoff.py +0 -0
  28. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/src/tests/test_items_client.py +0 -0
  29. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/src/tests/test_search_client.py +0 -0
  30. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/src/tests/test_transport_and_products.py +0 -0
  31. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/tests/__init__.py +0 -0
  32. {poelis_sdk-0.3.2 → poelis_sdk-0.3.4}/tests/test_browser_navigation.py +1 -1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: poelis-sdk
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Official Python SDK for Poelis
5
5
  Project-URL: Homepage, https://poelis.com
6
6
  Project-URL: Source, https://github.com/PoelisTechnologies/poelis-python-sdk
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "poelis-sdk"
7
- version = "0.3.2"
7
+ version = "0.3.4"
8
8
  description = "Official Python SDK for Poelis"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -7,7 +7,7 @@ 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
+ from .logging import configure_logging, debug_logging, get_logger, quiet_logging, verbose_logging
11
11
 
12
12
  __all__ = ["PoelisClient", "__version__", "configure_logging", "quiet_logging", "verbose_logging", "debug_logging", "get_logger"]
13
13
 
@@ -1,12 +1,20 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any, Dict, Mapping, Optional
4
3
  import os
5
- import time
6
4
  import random
5
+ import time
6
+ from typing import Any, Dict, Mapping, Optional
7
+
7
8
  import httpx
8
9
 
9
- from .exceptions import ClientError, HTTPError, NotFoundError, RateLimitError, ServerError, UnauthorizedError
10
+ from .exceptions import (
11
+ ClientError,
12
+ HTTPError,
13
+ NotFoundError,
14
+ RateLimitError,
15
+ ServerError,
16
+ UnauthorizedError,
17
+ )
10
18
 
11
19
  """HTTP transport abstraction for the Poelis SDK.
12
20
 
@@ -1,9 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Any, Dict, List, Optional
4
- from types import MethodType
5
3
  import re
6
4
  import time
5
+ from types import MethodType
6
+ from typing import Any, Dict, List, Optional
7
7
 
8
8
  from .org_validation import get_organization_context_message
9
9
 
@@ -140,39 +140,77 @@ class _Node:
140
140
  self._props_loaded_at = time.time()
141
141
  return self._props_cache
142
142
  # Try direct properties(itemId: ...) first; fallback to searchProperties
143
- q = (
144
- "query($iid: ID!) {\n"
143
+ # Attempt 1: query with parsedValue support
144
+ q_parsed = (
145
+ "query($iid: ID!) \n"
145
146
  " properties(itemId: $iid) {\n"
146
147
  " __typename\n"
147
- " ... on NumericProperty { integerPart exponent category }\n"
148
- " ... on TextProperty { value }\n"
149
- " ... on DateProperty { value }\n"
148
+ " ... on NumericProperty { id name category value parsedValue }\n"
149
+ " ... on TextProperty { id name value parsedValue }\n"
150
+ " ... on DateProperty { id name value }\n"
150
151
  " }\n"
151
152
  "}"
152
153
  )
153
154
  try:
154
- r = self._client._transport.graphql(q, {"iid": self._id})
155
+ r = self._client._transport.graphql(q_parsed, {"iid": self._id})
155
156
  r.raise_for_status()
156
157
  data = r.json()
157
158
  if "errors" in data:
158
- raise RuntimeError(data["errors"]) # trigger fallback
159
+ raise RuntimeError(data["errors"]) # try value-only shape
159
160
  self._props_cache = data.get("data", {}).get("properties", []) or []
160
161
  self._props_loaded_at = time.time()
161
162
  except Exception:
162
- q2 = (
163
- "query($iid: ID!, $limit: Int!, $offset: Int!) {\n"
164
- " searchProperties(q: \"*\", itemId: $iid, limit: $limit, offset: $offset) {\n"
165
- " hits { id workspaceId productId itemId propertyType name category value owner }\n"
163
+ # Attempt 2: value-only, legacy compatible
164
+ q_value_only = (
165
+ "query($iid: ID!) \n"
166
+ " properties(itemId: $iid) {\n"
167
+ " __typename\n"
168
+ " ... on NumericProperty { id name category value }\n"
169
+ " ... on TextProperty { id name value }\n"
170
+ " ... on DateProperty { id name value }\n"
166
171
  " }\n"
167
172
  "}"
168
173
  )
169
- r2 = self._client._transport.graphql(q2, {"iid": self._id, "limit": 100, "offset": 0})
170
- r2.raise_for_status()
171
- data2 = r2.json()
172
- if "errors" in data2:
173
- raise RuntimeError(data2["errors"]) # propagate
174
- self._props_cache = data2.get("data", {}).get("searchProperties", {}).get("hits", []) or []
175
- self._props_loaded_at = time.time()
174
+ try:
175
+ r = self._client._transport.graphql(q_value_only, {"iid": self._id})
176
+ r.raise_for_status()
177
+ data = r.json()
178
+ if "errors" in data:
179
+ raise RuntimeError(data["errors"]) # trigger fallback to search
180
+ self._props_cache = data.get("data", {}).get("properties", []) or []
181
+ self._props_loaded_at = time.time()
182
+ except Exception:
183
+ # Fallback to searchProperties
184
+ q2_parsed = (
185
+ "query($iid: ID!, $limit: Int!, $offset: Int!) {\n"
186
+ " searchProperties(q: \"*\", itemId: $iid, limit: $limit, offset: $offset) {\n"
187
+ " hits { id workspaceId productId itemId propertyType name category value parsedValue owner }\n"
188
+ " }\n"
189
+ "}"
190
+ )
191
+ try:
192
+ r2 = self._client._transport.graphql(q2_parsed, {"iid": self._id, "limit": 100, "offset": 0})
193
+ r2.raise_for_status()
194
+ data2 = r2.json()
195
+ if "errors" in data2:
196
+ raise RuntimeError(data2["errors"]) # try minimal
197
+ self._props_cache = data2.get("data", {}).get("searchProperties", {}).get("hits", []) or []
198
+ self._props_loaded_at = time.time()
199
+ except Exception:
200
+ q2_min = (
201
+ "query($iid: ID!, $limit: Int!, $offset: Int!) {\n"
202
+ " searchProperties(q: \"*\", itemId: $iid, limit: $limit, offset: $offset) {\n"
203
+ " hits { id workspaceId productId itemId propertyType name category value owner }\n"
204
+ " }\n"
205
+ "}"
206
+ )
207
+ r3 = self._client._transport.graphql(q2_min, {"iid": self._id, "limit": 100, "offset": 0})
208
+ r3.raise_for_status()
209
+ data3 = r3.json()
210
+ if "errors" in data3:
211
+ raise RuntimeError(data3["errors"]) # propagate
212
+ self._props_cache = data3.get("data", {}).get("searchProperties", {}).get("hits", []) or []
213
+ self._props_loaded_at = time.time()
176
214
  return self._props_cache
177
215
 
178
216
  def _props_key_map(self) -> Dict[str, Dict[str, Any]]:
@@ -419,6 +457,10 @@ class _PropWrapper:
419
457
  @property
420
458
  def value(self) -> Any: # type: ignore[override]
421
459
  p = self._raw
460
+ # Use parsedValue if available (new backend feature)
461
+ if "parsedValue" in p:
462
+ return p["parsedValue"]
463
+ # Fallback to legacy parsing logic for backward compatibility
422
464
  # searchProperties shape
423
465
  if "numericValue" in p and p.get("numericValue") is not None:
424
466
  return p["numericValue"]
@@ -1,16 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Optional
4
3
  import os
4
+ from typing import Optional
5
5
 
6
6
  from pydantic import BaseModel, Field, HttpUrl
7
+
7
8
  from ._transport import Transport
8
- from .products import ProductsClient
9
+ from .browser import Browser
9
10
  from .items import ItemsClient
11
+ from .logging import quiet_logging
12
+ from .products import ProductsClient
10
13
  from .search import SearchClient
11
14
  from .workspaces import WorkspacesClient
12
- from .browser import Browser
13
- from .logging import quiet_logging
14
15
 
15
16
  """Core client for the Poelis Python SDK.
16
17
 
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Generator, Any, Optional, Dict, List
3
+ from typing import Any, Dict, Generator, List, Optional
4
4
 
5
5
  from ._transport import Transport
6
6
 
@@ -0,0 +1,130 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, List, Optional, Union
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ """Pydantic models for SDK resources."""
8
+
9
+
10
+ class Product(BaseModel):
11
+ """Product resource representation."""
12
+
13
+ id: str = Field(min_length=1)
14
+ name: str = Field(min_length=1)
15
+ workspaceId: Optional[str] = None
16
+ code: Optional[str] = None
17
+ description: Optional[str] = None
18
+
19
+
20
+ class PaginatedProducts(BaseModel):
21
+ """Paginated response for products list."""
22
+
23
+ data: list[Product]
24
+ limit: int
25
+ offset: int
26
+
27
+
28
+ class PropertyValue(BaseModel):
29
+ """Base class for property values with typed access."""
30
+
31
+ raw_value: str = Field(alias="value")
32
+ parsed_value: Optional[Any] = Field(alias="parsedValue", default=None)
33
+
34
+ @property
35
+ def value(self) -> Any:
36
+ """Get the properly typed value, falling back to raw string if parsing failed."""
37
+ return self.parsed_value if self.parsed_value is not None else self.raw_value
38
+
39
+
40
+ class NumericProperty(BaseModel):
41
+ """Numeric property representation."""
42
+
43
+ id: str = Field(min_length=1)
44
+ item_id: str = Field(alias="itemId", min_length=1)
45
+ position: float
46
+ name: str = Field(min_length=1)
47
+ value: str
48
+ category: str
49
+ display_unit: Optional[str] = Field(alias="displayUnit", default=None)
50
+ owner: str = Field(min_length=1)
51
+ type: str = Field(min_length=1)
52
+ parsed_value: Optional[Union[int, float, List[Any], str]] = Field(alias="parsedValue", default=None)
53
+
54
+ @property
55
+ def typed_value(self) -> Union[int, float, List[Any], str]:
56
+ """Get the properly typed value, falling back to raw string if parsing failed."""
57
+ return self.parsed_value if self.parsed_value is not None else self.value
58
+
59
+
60
+ class TextProperty(BaseModel):
61
+ """Text property representation."""
62
+
63
+ id: str = Field(min_length=1)
64
+ item_id: str = Field(alias="itemId", min_length=1)
65
+ position: float
66
+ name: str = Field(min_length=1)
67
+ value: str
68
+ owner: str = Field(min_length=1)
69
+ type: str = Field(min_length=1)
70
+ parsed_value: Optional[Union[int, float, List[Any], str]] = Field(alias="parsedValue", default=None)
71
+
72
+ @property
73
+ def typed_value(self) -> Union[int, float, List[Any], str]:
74
+ """Get the properly typed value, falling back to raw string if parsing failed."""
75
+ return self.parsed_value if self.parsed_value is not None else self.value
76
+
77
+
78
+ class DateProperty(BaseModel):
79
+ """Date property representation."""
80
+
81
+ id: str = Field(min_length=1)
82
+ item_id: str = Field(alias="itemId", min_length=1)
83
+ position: float
84
+ name: str = Field(min_length=1)
85
+ value: str
86
+ owner: str = Field(min_length=1)
87
+ type: str = Field(min_length=1)
88
+ parsed_value: Optional[str] = Field(alias="parsedValue", default=None)
89
+
90
+ @property
91
+ def typed_value(self) -> str:
92
+ """Get the properly typed value, falling back to raw string if parsing failed."""
93
+ return self.parsed_value if self.parsed_value is not None else self.value
94
+
95
+
96
+ class PropertySearchResult(BaseModel):
97
+ """Property search result with unified fields across all property types."""
98
+
99
+ id: str = Field(min_length=1)
100
+ workspace_id: str = Field(alias="workspaceId", min_length=1)
101
+ product_id: str = Field(alias="productId", min_length=1)
102
+ item_id: str = Field(alias="itemId", min_length=1)
103
+ property_type: str = Field(alias="propertyType", min_length=1)
104
+ name: str = Field(min_length=1)
105
+ category: Optional[str] = None
106
+ display_unit: Optional[str] = Field(alias="displayUnit", default=None)
107
+ value: Any # Raw value from GraphQL
108
+ parsed_value: Optional[Union[int, float, List[Any], str]] = Field(alias="parsedValue", default=None)
109
+ owner: str = Field(min_length=1)
110
+ created_by: str = Field(alias="createdBy", min_length=1)
111
+ created_at: str = Field(alias="createdAt", min_length=1)
112
+ updated_at: str = Field(alias="updatedAt", min_length=1)
113
+
114
+ @property
115
+ def typed_value(self) -> Union[int, float, List[Any], str]:
116
+ """Get the properly typed value, falling back to raw string if parsing failed."""
117
+ return self.parsed_value if self.parsed_value is not None else self.value
118
+
119
+
120
+ class PropertySearchResponse(BaseModel):
121
+ """Response for property search queries."""
122
+
123
+ query: str
124
+ hits: List[PropertySearchResult]
125
+ total: int
126
+ limit: int
127
+ offset: int
128
+ processing_time_ms: int = Field(alias="processingTimeMs")
129
+
130
+
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Generator, Optional, TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, Generator, Optional
4
4
 
5
5
  from ._transport import Transport
6
6
  from .models import PaginatedProducts, Product
@@ -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 value owner }\n"
57
+ " hits { id workspaceId productId itemId propertyType name category value parsedValue owner }\n"
58
58
  " }\n"
59
59
  "}"
60
60
  )
@@ -3,7 +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
+ from .org_validation import filter_by_organization, validate_workspace_organization
7
7
 
8
8
  """Workspaces GraphQL client."""
9
9
 
@@ -6,6 +6,7 @@ Skips by default unless POELIS_API_KEY and POELIS_ORG_ID are set.
6
6
  from __future__ import annotations
7
7
 
8
8
  import os
9
+
9
10
  import pytest
10
11
 
11
12
  from poelis_sdk import PoelisClient
@@ -0,0 +1,212 @@
1
+ """Tests for typed property values functionality."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from poelis_sdk.browser import _PropWrapper
8
+ from poelis_sdk.models import DateProperty, NumericProperty, PropertySearchResult, TextProperty
9
+
10
+ if TYPE_CHECKING:
11
+ from _pytest.capture import CaptureFixture # noqa: F401
12
+ from _pytest.fixtures import FixtureRequest # noqa: F401
13
+ from _pytest.logging import LogCaptureFixture # noqa: F401
14
+ from _pytest.monkeypatch import MonkeyPatch # noqa: F401
15
+ from pytest_mock.plugin import MockerFixture # noqa: F401
16
+
17
+
18
+ class TestTypedPropertyModels:
19
+ """Test Pydantic models for typed property values."""
20
+
21
+ def test_numeric_property_with_parsed_value(self) -> None:
22
+ """Test NumericProperty model with parsedValue field."""
23
+ prop_data = {
24
+ "id": "prop1",
25
+ "itemId": "item1",
26
+ "position": 1.0,
27
+ "name": "Weight",
28
+ "value": "42.5",
29
+ "category": "Physical",
30
+ "displayUnit": "kg",
31
+ "owner": "user1",
32
+ "type": "numeric",
33
+ "parsedValue": 42.5
34
+ }
35
+
36
+ prop = NumericProperty(**prop_data)
37
+ assert prop.typed_value == 42.5
38
+ assert isinstance(prop.typed_value, float)
39
+ assert prop.value == "42.5" # Raw value remains string
40
+
41
+ def test_numeric_property_without_parsed_value(self) -> None:
42
+ """Test NumericProperty model without parsedValue field (fallback)."""
43
+ prop_data = {
44
+ "id": "prop1",
45
+ "itemId": "item1",
46
+ "position": 1.0,
47
+ "name": "Weight",
48
+ "value": "42.5",
49
+ "category": "Physical",
50
+ "displayUnit": "kg",
51
+ "owner": "user1",
52
+ "type": "numeric"
53
+ }
54
+
55
+ prop = NumericProperty(**prop_data)
56
+ assert prop.typed_value == "42.5" # Falls back to raw string
57
+ assert prop.value == "42.5"
58
+
59
+ def test_text_property_with_parsed_array(self) -> None:
60
+ """Test TextProperty model with parsed array value."""
61
+ prop_data = {
62
+ "id": "prop2",
63
+ "itemId": "item1",
64
+ "position": 2.0,
65
+ "name": "Tags",
66
+ "value": '["tag1", "tag2", "tag3"]',
67
+ "owner": "user1",
68
+ "type": "text",
69
+ "parsedValue": ["tag1", "tag2", "tag3"]
70
+ }
71
+
72
+ prop = TextProperty(**prop_data)
73
+ assert prop.typed_value == ["tag1", "tag2", "tag3"]
74
+ assert isinstance(prop.typed_value, list)
75
+ assert prop.value == '["tag1", "tag2", "tag3"]' # Raw value remains string
76
+
77
+ def test_text_property_with_parsed_integer(self) -> None:
78
+ """Test TextProperty model with parsed integer value."""
79
+ prop_data = {
80
+ "id": "prop3",
81
+ "itemId": "item1",
82
+ "position": 3.0,
83
+ "name": "Count",
84
+ "value": "100",
85
+ "owner": "user1",
86
+ "type": "text",
87
+ "parsedValue": 100
88
+ }
89
+
90
+ prop = TextProperty(**prop_data)
91
+ assert prop.typed_value == 100
92
+ assert isinstance(prop.typed_value, int)
93
+ assert prop.value == "100" # Raw value remains string
94
+
95
+ def test_date_property_with_parsed_value(self) -> None:
96
+ """Test DateProperty model with parsedValue field."""
97
+ prop_data = {
98
+ "id": "prop4",
99
+ "itemId": "item1",
100
+ "position": 4.0,
101
+ "name": "Created Date",
102
+ "value": "2024-01-15",
103
+ "owner": "user1",
104
+ "type": "date",
105
+ "parsedValue": "2024-01-15"
106
+ }
107
+
108
+ prop = DateProperty(**prop_data)
109
+ assert prop.typed_value == "2024-01-15"
110
+ assert isinstance(prop.typed_value, str)
111
+ assert prop.value == "2024-01-15"
112
+
113
+ def test_property_search_result_with_parsed_value(self) -> None:
114
+ """Test PropertySearchResult model with parsedValue field."""
115
+ result_data = {
116
+ "id": "prop5",
117
+ "workspaceId": "ws1",
118
+ "productId": "prod1",
119
+ "itemId": "item1",
120
+ "propertyType": "numeric",
121
+ "name": "Temperature",
122
+ "category": "Environmental",
123
+ "displayUnit": "°C",
124
+ "value": "25.3",
125
+ "parsedValue": 25.3,
126
+ "owner": "user1",
127
+ "createdBy": "user1",
128
+ "createdAt": "2024-01-15T10:00:00Z",
129
+ "updatedAt": "2024-01-15T10:00:00Z"
130
+ }
131
+
132
+ result = PropertySearchResult(**result_data)
133
+ assert result.typed_value == 25.3
134
+ assert isinstance(result.typed_value, float)
135
+ assert result.value == "25.3" # Raw value remains string
136
+
137
+
138
+ class TestPropWrapper:
139
+ """Test _PropWrapper class with typed values."""
140
+
141
+ def test_prop_wrapper_with_parsed_value(self) -> None:
142
+ """Test _PropWrapper with parsedValue field."""
143
+ prop_data = {
144
+ "id": "prop1",
145
+ "name": "Weight",
146
+ "value": "42.5",
147
+ "parsedValue": 42.5,
148
+ "category": "Physical"
149
+ }
150
+
151
+ wrapper = _PropWrapper(prop_data)
152
+ assert wrapper.value == 42.5
153
+ assert isinstance(wrapper.value, float)
154
+ assert wrapper.category == "Physical"
155
+
156
+ def test_prop_wrapper_without_parsed_value_fallback(self) -> None:
157
+ """Test _PropWrapper fallback when parsedValue is not available."""
158
+ prop_data = {
159
+ "id": "prop1",
160
+ "name": "Weight",
161
+ "value": "42.5",
162
+ "category": "Physical"
163
+ }
164
+
165
+ wrapper = _PropWrapper(prop_data)
166
+ assert wrapper.value == "42.5" # Falls back to raw value
167
+ assert isinstance(wrapper.value, str)
168
+ assert wrapper.category == "Physical"
169
+
170
+ def test_prop_wrapper_with_parsed_array(self) -> None:
171
+ """Test _PropWrapper with parsed array value."""
172
+ prop_data = {
173
+ "id": "prop2",
174
+ "name": "Tags",
175
+ "value": '["tag1", "tag2"]',
176
+ "parsedValue": ["tag1", "tag2"],
177
+ "category": "Metadata"
178
+ }
179
+
180
+ wrapper = _PropWrapper(prop_data)
181
+ assert wrapper.value == ["tag1", "tag2"]
182
+ assert isinstance(wrapper.value, list)
183
+ assert wrapper.category == "Metadata"
184
+
185
+ def test_prop_wrapper_legacy_numeric_parsing(self) -> None:
186
+ """Test _PropWrapper with legacy numeric parsing (integerPart/exponent)."""
187
+ prop_data = {
188
+ "id": "prop3",
189
+ "name": "Legacy Numeric",
190
+ "integerPart": 42,
191
+ "exponent": 1,
192
+ "category": "Legacy"
193
+ }
194
+
195
+ wrapper = _PropWrapper(prop_data)
196
+ assert wrapper.value == 420 # 42 * 10^1
197
+ assert isinstance(wrapper.value, int)
198
+ assert wrapper.category == "Legacy"
199
+
200
+ def test_prop_wrapper_legacy_search_properties(self) -> None:
201
+ """Test _PropWrapper with legacy searchProperties format."""
202
+ prop_data = {
203
+ "id": "prop4",
204
+ "name": "Search Numeric",
205
+ "numericValue": 123.45,
206
+ "category": "Search"
207
+ }
208
+
209
+ wrapper = _PropWrapper(prop_data)
210
+ assert wrapper.value == 123.45
211
+ assert isinstance(wrapper.value, float)
212
+ assert wrapper.category == "Search"
@@ -538,7 +538,7 @@ wheels = [
538
538
 
539
539
  [[package]]
540
540
  name = "poelis-sdk"
541
- version = "0.3.2"
541
+ version = "0.3.4"
542
542
  source = { editable = "." }
543
543
  dependencies = [
544
544
  { name = "build" },
@@ -1,27 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Optional
4
-
5
- from pydantic import BaseModel, Field
6
-
7
- """Pydantic models for SDK resources."""
8
-
9
-
10
- class Product(BaseModel):
11
- """Product resource representation."""
12
-
13
- id: str = Field(min_length=1)
14
- name: str = Field(min_length=1)
15
- workspaceId: Optional[str] = None
16
- code: Optional[str] = None
17
- description: Optional[str] = None
18
-
19
-
20
- class PaginatedProducts(BaseModel):
21
- """Paginated response for products list."""
22
-
23
- data: list[Product]
24
- limit: int
25
- offset: int
26
-
27
-
File without changes
File without changes
File without changes
@@ -8,7 +8,6 @@ from __future__ import annotations
8
8
 
9
9
  from typing import TYPE_CHECKING
10
10
 
11
-
12
11
  from poelis_sdk import PoelisClient
13
12
 
14
13
  if TYPE_CHECKING:
@@ -37,6 +36,7 @@ def test_client_api_key_headers(monkeypatch: "MonkeyPatch") -> None:
37
36
  """When api_key and org_id are provided, use API key and X-Poelis-Org headers by default."""
38
37
 
39
38
  import httpx
39
+
40
40
  from poelis_sdk.client import Transport as _T
41
41
 
42
42
  class _Tpt(httpx.BaseTransport):
File without changes
@@ -5,8 +5,8 @@ These tests avoid reliance on IPython and focus on programmatic APIs.
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- from typing import TYPE_CHECKING, Any, Dict
9
8
  import json
9
+ from typing import TYPE_CHECKING, Any, Dict
10
10
 
11
11
  import httpx
12
12