@workos/oagen-emitters 0.2.0 → 0.3.0

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.
Files changed (110) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/.oxfmtrc.json +8 -1
  3. package/.release-please-manifest.json +1 -1
  4. package/CHANGELOG.md +15 -0
  5. package/README.md +129 -0
  6. package/dist/index.d.mts +10 -1
  7. package/dist/index.d.mts.map +1 -1
  8. package/dist/index.mjs +11943 -2728
  9. package/dist/index.mjs.map +1 -1
  10. package/docs/sdk-architecture/go.md +338 -0
  11. package/docs/sdk-architecture/php.md +315 -0
  12. package/docs/sdk-architecture/python.md +511 -0
  13. package/oagen.config.ts +298 -2
  14. package/package.json +9 -5
  15. package/scripts/generate-php.js +13 -0
  16. package/scripts/git-push-with-published-oagen.sh +21 -0
  17. package/smoke/sdk-dotnet.ts +17 -3
  18. package/smoke/sdk-elixir.ts +17 -3
  19. package/smoke/sdk-go.ts +137 -46
  20. package/smoke/sdk-kotlin.ts +23 -4
  21. package/smoke/sdk-node.ts +15 -3
  22. package/smoke/sdk-php.ts +28 -26
  23. package/smoke/sdk-python.ts +5 -2
  24. package/smoke/sdk-ruby.ts +17 -3
  25. package/smoke/sdk-rust.ts +16 -3
  26. package/src/go/client.ts +141 -0
  27. package/src/go/enums.ts +196 -0
  28. package/src/go/fixtures.ts +212 -0
  29. package/src/go/index.ts +81 -0
  30. package/src/go/manifest.ts +36 -0
  31. package/src/go/models.ts +254 -0
  32. package/src/go/naming.ts +191 -0
  33. package/src/go/resources.ts +827 -0
  34. package/src/go/tests.ts +751 -0
  35. package/src/go/type-map.ts +82 -0
  36. package/src/go/wrappers.ts +261 -0
  37. package/src/index.ts +3 -0
  38. package/src/node/client.ts +167 -122
  39. package/src/node/enums.ts +13 -4
  40. package/src/node/errors.ts +42 -233
  41. package/src/node/field-plan.ts +726 -0
  42. package/src/node/fixtures.ts +15 -5
  43. package/src/node/index.ts +65 -16
  44. package/src/node/models.ts +264 -96
  45. package/src/node/naming.ts +52 -25
  46. package/src/node/resources.ts +621 -172
  47. package/src/node/sdk-errors.ts +41 -0
  48. package/src/node/tests.ts +71 -27
  49. package/src/node/type-map.ts +4 -2
  50. package/src/node/utils.ts +56 -64
  51. package/src/node/wrappers.ts +151 -0
  52. package/src/php/client.ts +171 -0
  53. package/src/php/enums.ts +67 -0
  54. package/src/php/errors.ts +9 -0
  55. package/src/php/fixtures.ts +181 -0
  56. package/src/php/index.ts +96 -0
  57. package/src/php/manifest.ts +36 -0
  58. package/src/php/models.ts +310 -0
  59. package/src/php/naming.ts +298 -0
  60. package/src/php/resources.ts +561 -0
  61. package/src/php/tests.ts +533 -0
  62. package/src/php/type-map.ts +90 -0
  63. package/src/php/utils.ts +18 -0
  64. package/src/php/wrappers.ts +151 -0
  65. package/src/python/client.ts +337 -0
  66. package/src/python/enums.ts +313 -0
  67. package/src/python/fixtures.ts +196 -0
  68. package/src/python/index.ts +95 -0
  69. package/src/python/manifest.ts +38 -0
  70. package/src/python/models.ts +688 -0
  71. package/src/python/naming.ts +209 -0
  72. package/src/python/resources.ts +1322 -0
  73. package/src/python/tests.ts +1335 -0
  74. package/src/python/type-map.ts +93 -0
  75. package/src/python/wrappers.ts +191 -0
  76. package/src/shared/model-utils.ts +255 -0
  77. package/src/shared/naming-utils.ts +107 -0
  78. package/src/shared/non-spec-services.ts +54 -0
  79. package/src/shared/resolved-ops.ts +109 -0
  80. package/src/shared/wrapper-utils.ts +59 -0
  81. package/test/go/client.test.ts +92 -0
  82. package/test/go/enums.test.ts +132 -0
  83. package/test/go/errors.test.ts +9 -0
  84. package/test/go/models.test.ts +265 -0
  85. package/test/go/resources.test.ts +408 -0
  86. package/test/go/tests.test.ts +143 -0
  87. package/test/node/client.test.ts +199 -94
  88. package/test/node/enums.test.ts +75 -3
  89. package/test/node/errors.test.ts +2 -41
  90. package/test/node/models.test.ts +109 -20
  91. package/test/node/naming.test.ts +37 -4
  92. package/test/node/resources.test.ts +662 -30
  93. package/test/node/serializers.test.ts +36 -7
  94. package/test/node/type-map.test.ts +11 -0
  95. package/test/php/client.test.ts +94 -0
  96. package/test/php/enums.test.ts +173 -0
  97. package/test/php/errors.test.ts +9 -0
  98. package/test/php/models.test.ts +497 -0
  99. package/test/php/resources.test.ts +644 -0
  100. package/test/php/tests.test.ts +118 -0
  101. package/test/python/client.test.ts +200 -0
  102. package/test/python/enums.test.ts +228 -0
  103. package/test/python/errors.test.ts +16 -0
  104. package/test/python/manifest.test.ts +74 -0
  105. package/test/python/models.test.ts +716 -0
  106. package/test/python/resources.test.ts +617 -0
  107. package/test/python/tests.test.ts +202 -0
  108. package/src/node/common.ts +0 -273
  109. package/src/node/config.ts +0 -71
  110. package/src/node/serializers.ts +0 -744
@@ -0,0 +1,511 @@
1
+ # Python SDK Architecture
2
+
3
+ Fresh Scenario B design — no existing SDK to preserve.
4
+
5
+ ## Architecture Overview
6
+
7
+ - **Main client**: `WorkOS` class with HTTP methods (`_request`, `get`, `post`, `put`, `patch`, `delete`) and resource accessors (e.g., `workos.organizations`).
8
+ - **Resource classes**: One per service, constructor receives `WorkOS` client, methods return deserialized dataclass instances.
9
+ - **Single type system**: Dataclasses with snake_case fields matching wire format — no separate domain/wire layers.
10
+ - **Deserialization**: `from_dict` class methods on dataclasses; `to_dict` instance methods for serialization.
11
+ - **Pagination**: `SyncPage[T]` with cursor-based `after` param, `auto_paging_iter()` generator.
12
+ - **Error hierarchy**: Status-code-specific exception classes extending base `WorkOSError`.
13
+ - **Constructor**: Accepts `api_key` keyword or reads `WORKOS_API_KEY` env var.
14
+ - **HTTP client**: `httpx` for sync HTTP with connection pooling.
15
+
16
+ ## Naming Conventions
17
+
18
+ | Concept | Convention | Example |
19
+ | ---------------- | ----------- | ------------------------------------------------ |
20
+ | Class | PascalCase | `Organization`, `UserManagement` |
21
+ | Method | snake_case | `list_organizations`, `create_organization` |
22
+ | Field | snake_case | `allow_profiles_outside_organization` |
23
+ | File | snake_case | `organization.py` |
24
+ | Directory | snake_case | `organizations/`, `user_management/` |
25
+ | Module | snake_case | `workos.organizations` |
26
+ | Service property | snake_case | `workos.organizations`, `workos.user_management` |
27
+ | Enum member | UPPER_SNAKE | `OrganizationDomainVerificationStrategy.DNS` |
28
+ | Package | snake_case | `workos` |
29
+
30
+ ### Overlay Resolution
31
+
32
+ All service-derived names are resolved through the overlay before falling back to the default PascalCase convention. Method names and type names also check the overlay for backwards-compatible naming.
33
+
34
+ ## Type Mapping
35
+
36
+ | IR TypeRef | Python Type |
37
+ | -------------------- | --------------- |
38
+ | `string` | `str` |
39
+ | `string` (date) | `str` |
40
+ | `string` (date-time) | `str` |
41
+ | `string` (uuid) | `str` |
42
+ | `string` (binary) | `bytes` |
43
+ | `integer` | `int` |
44
+ | `number` | `float` |
45
+ | `boolean` | `bool` |
46
+ | `unknown` | `Any` |
47
+ | `array(T)` | `List[T]` |
48
+ | `model(Name)` | `Name` |
49
+ | `enum(Name)` | `Name` |
50
+ | `nullable(T)` | `Optional[T]` |
51
+ | `union(V1,V2)` | `Union[V1, V2]` |
52
+ | `map(V)` | `Dict[str, V]` |
53
+ | `literal(v)` | `Literal["v"]` |
54
+
55
+ ## Model Pattern
56
+
57
+ ```python
58
+ from dataclasses import dataclass
59
+ from typing import Optional, List, Dict, Any
60
+
61
+
62
+ @dataclass
63
+ class Organization:
64
+ """An organization within WorkOS."""
65
+
66
+ id: str
67
+ name: str
68
+ allow_profiles_outside_organization: bool
69
+ domains: List["OrganizationDomain"]
70
+ created_at: str
71
+ updated_at: str
72
+ external_id: Optional[str] = None
73
+ stripe_customer_id: Optional[str] = None
74
+ metadata: Optional[Dict[str, str]] = None
75
+
76
+ @classmethod
77
+ def from_dict(cls, data: Dict[str, Any]) -> "Organization":
78
+ return cls(
79
+ id=data["id"],
80
+ name=data["name"],
81
+ allow_profiles_outside_organization=data["allow_profiles_outside_organization"],
82
+ domains=[OrganizationDomain.from_dict(d) for d in data.get("domains", [])],
83
+ created_at=data["created_at"],
84
+ updated_at=data["updated_at"],
85
+ external_id=data.get("external_id"),
86
+ stripe_customer_id=data.get("stripe_customer_id"),
87
+ metadata=data.get("metadata"),
88
+ )
89
+
90
+ def to_dict(self) -> Dict[str, Any]:
91
+ result: Dict[str, Any] = {
92
+ "id": self.id,
93
+ "name": self.name,
94
+ "allow_profiles_outside_organization": self.allow_profiles_outside_organization,
95
+ "domains": [d.to_dict() for d in self.domains],
96
+ "created_at": self.created_at,
97
+ "updated_at": self.updated_at,
98
+ }
99
+ if self.external_id is not None:
100
+ result["external_id"] = self.external_id
101
+ if self.stripe_customer_id is not None:
102
+ result["stripe_customer_id"] = self.stripe_customer_id
103
+ if self.metadata is not None:
104
+ result["metadata"] = self.metadata
105
+ return result
106
+ ```
107
+
108
+ Key patterns:
109
+
110
+ - Dataclasses with type annotations
111
+ - Required fields first, optional fields after (with `= None`)
112
+ - `from_dict` class method for deserialization from API response
113
+ - `to_dict` instance method for serialization
114
+ - Nested model refs deserialized recursively via their own `from_dict`
115
+ - Array fields use list comprehension for nested deserialization
116
+ - Optional fields use `data.get()` for safe access
117
+
118
+ ## Enum Pattern
119
+
120
+ ```python
121
+ from enum import Enum
122
+
123
+
124
+ class OrganizationDomainVerificationStrategy(str, Enum):
125
+ """Verification strategy for organization domains."""
126
+
127
+ DNS = "dns"
128
+ MANUAL = "manual"
129
+ ```
130
+
131
+ Key patterns:
132
+
133
+ - Inherit from `StrEnum` (Python 3.11+)
134
+ - Member names: UPPER_SNAKE_CASE
135
+ - Member values: original string from spec
136
+
137
+ ## Resource Pattern
138
+
139
+ ```python
140
+ from typing import Optional
141
+ from ..types import RequestOptions
142
+ from .models import Organization, CreateOrganizationParams
143
+ from ..pagination import SyncPage
144
+
145
+
146
+ class Organizations:
147
+ """Resource for managing organizations."""
148
+
149
+ def __init__(self, client: "WorkOS") -> None:
150
+ self._client = client
151
+
152
+ def list_organizations(
153
+ self,
154
+ *,
155
+ limit: Optional[int] = None,
156
+ before: Optional[str] = None,
157
+ after: Optional[str] = None,
158
+ order: Optional[str] = None,
159
+ request_options: Optional[RequestOptions] = None,
160
+ ) -> SyncPage[Organization]:
161
+ """List all organizations.
162
+
163
+ Args:
164
+ limit: Maximum number of records to return.
165
+ before: Pagination cursor for previous page.
166
+ after: Pagination cursor for next page.
167
+ order: Sort order.
168
+ request_options: Per-request options (extra headers, timeout).
169
+
170
+ Returns:
171
+ A paginated list of organizations.
172
+ """
173
+ return self._client._request_page(
174
+ method="get",
175
+ path="organizations",
176
+ model=Organization,
177
+ params={k: v for k, v in {
178
+ "limit": limit,
179
+ "before": before,
180
+ "after": after,
181
+ "order": order,
182
+ }.items() if v is not None},
183
+ request_options=request_options,
184
+ )
185
+
186
+ def create_organization(
187
+ self,
188
+ *,
189
+ name: str,
190
+ domain_data: Optional[list] = None,
191
+ external_id: Optional[str] = None,
192
+ metadata: Optional[dict] = None,
193
+ idempotency_key: Optional[str] = None,
194
+ request_options: Optional[RequestOptions] = None,
195
+ ) -> Organization:
196
+ """Create a new organization.
197
+
198
+ Args:
199
+ name: The name of the organization.
200
+ domain_data: Domain configuration.
201
+ external_id: An external identifier.
202
+ metadata: Key-value metadata.
203
+ idempotency_key: Idempotency key for safe retries.
204
+ request_options: Per-request options.
205
+
206
+ Returns:
207
+ The created organization.
208
+ """
209
+ body = {k: v for k, v in {
210
+ "name": name,
211
+ "domain_data": domain_data,
212
+ "external_id": external_id,
213
+ "metadata": metadata,
214
+ }.items() if v is not None}
215
+ return self._client._request(
216
+ method="post",
217
+ path="organizations",
218
+ body=body,
219
+ model=Organization,
220
+ idempotency_key=idempotency_key,
221
+ request_options=request_options,
222
+ )
223
+
224
+ def get_organization(
225
+ self,
226
+ organization_id: str,
227
+ *,
228
+ request_options: Optional[RequestOptions] = None,
229
+ ) -> Organization:
230
+ """Get an organization by ID.
231
+
232
+ Args:
233
+ organization_id: The ID of the organization.
234
+ request_options: Per-request options.
235
+
236
+ Returns:
237
+ The organization.
238
+ """
239
+ return self._client._request(
240
+ method="get",
241
+ path=f"organizations/{organization_id}",
242
+ model=Organization,
243
+ request_options=request_options,
244
+ )
245
+
246
+ def delete_organization(
247
+ self,
248
+ organization_id: str,
249
+ *,
250
+ request_options: Optional[RequestOptions] = None,
251
+ ) -> None:
252
+ """Delete an organization.
253
+
254
+ Args:
255
+ organization_id: The ID of the organization.
256
+ request_options: Per-request options.
257
+ """
258
+ self._client._request(
259
+ method="delete",
260
+ path=f"organizations/{organization_id}",
261
+ request_options=request_options,
262
+ )
263
+ ```
264
+
265
+ Key patterns:
266
+
267
+ - Constructor takes `client: "WorkOS"`
268
+ - All parameters after the first positional are keyword-only (`*`)
269
+ - List methods return `SyncPage[T]` with pagination params
270
+ - Create/update methods: build body dict, POST, return deserialized model
271
+ - Get methods: GET with path interpolation via f-string, return deserialized model
272
+ - Delete methods: return `None`
273
+ - Idempotent POSTs: `idempotency_key` as standalone keyword parameter
274
+ - Every method takes optional `request_options` as the last parameter
275
+ - Google-style docstrings
276
+
277
+ ## Pagination Pattern
278
+
279
+ ```python
280
+ from dataclasses import dataclass
281
+ from typing import TypeVar, Generic, List, Optional, Iterator, Callable, Dict, Any
282
+
283
+ T = TypeVar("T")
284
+
285
+
286
+ @dataclass
287
+ class SyncPage(Generic[T]):
288
+ """A page of results with auto-pagination support."""
289
+
290
+ data: List[T]
291
+ list_metadata: Dict[str, Any]
292
+ _fetch_page: Optional[Callable[..., "SyncPage[T]"]] = None
293
+
294
+ @property
295
+ def after(self) -> Optional[str]:
296
+ return self.list_metadata.get("after")
297
+
298
+ def has_more(self) -> bool:
299
+ return self.after is not None
300
+
301
+ def auto_paging_iter(self) -> Iterator[T]:
302
+ page = self
303
+ while True:
304
+ yield from page.data
305
+ if not page.has_more() or page._fetch_page is None:
306
+ break
307
+ page = page._fetch_page(after=page.after)
308
+ ```
309
+
310
+ ## Error Handling
311
+
312
+ ```python
313
+ class WorkOSError(Exception):
314
+ """Base exception for all WorkOS errors."""
315
+
316
+ def __init__(self, message: str, *, status_code: int | None = None, request_id: str | None = None):
317
+ super().__init__(message)
318
+ self.status_code = status_code
319
+ self.request_id = request_id
320
+
321
+
322
+ class AuthenticationError(WorkOSError):
323
+ """401 Unauthorized."""
324
+ pass
325
+
326
+ class NotFoundError(WorkOSError):
327
+ """404 Not Found."""
328
+ pass
329
+
330
+ class BadRequestError(WorkOSError):
331
+ """400 Bad Request."""
332
+ pass
333
+
334
+ class UnprocessableEntityError(WorkOSError):
335
+ """422 Unprocessable Entity."""
336
+ pass
337
+
338
+ class RateLimitExceededError(WorkOSError):
339
+ """429 Rate Limited."""
340
+ pass
341
+
342
+ class ServerError(WorkOSError):
343
+ """500+ Server Error."""
344
+ pass
345
+
346
+ class ConfigurationError(WorkOSError):
347
+ """Missing or invalid configuration (e.g., no API key)."""
348
+ pass
349
+ ```
350
+
351
+ | Exception Class | Status Code |
352
+ | -------------------------- | ----------- |
353
+ | `BadRequestError` | 400 |
354
+ | `AuthenticationError` | 401 |
355
+ | `NotFoundError` | 404 |
356
+ | `UnprocessableEntityError` | 422 |
357
+ | `RateLimitExceededError` | 429 |
358
+ | `ServerError` | 500+ |
359
+ | `ConfigurationError` | runtime |
360
+
361
+ ## Client Architecture
362
+
363
+ ```python
364
+ import os
365
+ from typing import Optional, Type, TypeVar, Dict, Any
366
+ import httpx
367
+
368
+ T = TypeVar("T")
369
+
370
+
371
+ class WorkOS:
372
+ """WorkOS API client."""
373
+
374
+ def __init__(
375
+ self,
376
+ *,
377
+ api_key: Optional[str] = None,
378
+ base_url: str = "https://api.workos.com",
379
+ timeout: float = 30.0,
380
+ max_retries: int = 3,
381
+ ) -> None:
382
+ self._api_key = api_key or os.environ.get("WORKOS_API_KEY")
383
+ if not self._api_key:
384
+ raise ConfigurationError("No API key provided")
385
+ self._base_url = base_url.rstrip("/")
386
+ self._timeout = timeout
387
+ self._max_retries = max_retries
388
+ self._client = httpx.Client(timeout=timeout)
389
+
390
+ # Resource accessors
391
+ self.organizations = Organizations(self)
392
+ # ... other resources
393
+
394
+ def _request(
395
+ self,
396
+ method: str,
397
+ path: str,
398
+ *,
399
+ params: Optional[Dict[str, Any]] = None,
400
+ body: Optional[Dict[str, Any]] = None,
401
+ model: Optional[Type[T]] = None,
402
+ idempotency_key: Optional[str] = None,
403
+ request_options: Optional[RequestOptions] = None,
404
+ ) -> T | None:
405
+ # Build request, handle retries, deserialize response
406
+ ...
407
+ ```
408
+
409
+ ## HTTP Client (Retry Logic)
410
+
411
+ - `MAX_RETRIES = 3`
412
+ - `INITIAL_RETRY_DELAY = 0.5` (seconds)
413
+ - `MAX_RETRY_DELAY = 8.0` (seconds)
414
+ - `RETRY_MULTIPLIER = 2.0`
415
+ - Retryable statuses: `429, 500, 502, 503, 504`
416
+ - Backoff: `min(INITIAL_RETRY_DELAY * RETRY_MULTIPLIER ** attempt, MAX_RETRY_DELAY)`
417
+ - Jitter: `delay * (0.5 + random())`
418
+ - Respect `Retry-After` header for 429
419
+ - Auto-generated UUID idempotency keys for POST, reused across retries
420
+
421
+ ## Testing Pattern
422
+
423
+ Framework: pytest + pytest-httpx
424
+
425
+ ```python
426
+ import pytest
427
+ from workos import WorkOS
428
+ from workos.organizations.models import Organization
429
+
430
+
431
+ @pytest.fixture
432
+ def workos():
433
+ return WorkOS(api_key="sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU")
434
+
435
+
436
+ class TestOrganizations:
437
+ def test_list_organizations(self, workos, httpx_mock):
438
+ httpx_mock.add_response(
439
+ url="https://api.workos.com/organizations",
440
+ json=load_fixture("list_organizations.json"),
441
+ )
442
+ page = workos.organizations.list_organizations()
443
+ assert len(page.data) == 7
444
+ assert isinstance(page.data[0], Organization)
445
+
446
+ def test_get_organization(self, workos, httpx_mock):
447
+ httpx_mock.add_response(
448
+ url="https://api.workos.com/organizations/org_01234",
449
+ json=load_fixture("organization.json"),
450
+ )
451
+ org = workos.organizations.get_organization("org_01234")
452
+ assert org.id == "org_01234"
453
+ assert isinstance(org, Organization)
454
+
455
+ def test_delete_organization(self, workos, httpx_mock):
456
+ httpx_mock.add_response(
457
+ url="https://api.workos.com/organizations/org_01234",
458
+ status_code=204,
459
+ )
460
+ result = workos.organizations.delete_organization("org_01234")
461
+ assert result is None
462
+
463
+ def test_unauthorized_error(self, workos, httpx_mock):
464
+ httpx_mock.add_response(
465
+ url="https://api.workos.com/organizations",
466
+ status_code=401,
467
+ json={"message": "Unauthorized"},
468
+ )
469
+ with pytest.raises(AuthenticationError):
470
+ workos.organizations.list_organizations()
471
+ ```
472
+
473
+ ## Directory Structure
474
+
475
+ ```
476
+ {namespace}/
477
+ ├── __init__.py # Package init, re-exports WorkOS client
478
+ ├── _client.py # Main WorkOS client class
479
+ ├── _config.py # Configuration, RequestOptions
480
+ ├── _errors.py # Error hierarchy
481
+ ├── _pagination.py # SyncPage, auto-pagination
482
+ ├── _types.py # Shared type aliases, RequestOptions
483
+ ├── {service}/
484
+ │ ├── __init__.py # Re-exports models and resource
485
+ │ ├── _resource.py # Resource class (e.g., Organizations)
486
+ │ ├── models.py # Dataclass models and enums
487
+ │ └── fixtures/ # JSON test fixtures
488
+ ├── tests/
489
+ │ ├── conftest.py # Shared fixtures (workos client, load_fixture)
490
+ │ └── test_{service}.py # Per-service test file
491
+ ├── py.typed # PEP 561 marker
492
+ └── pyproject.toml # Package manifest
493
+ ```
494
+
495
+ ## Structural Guidelines
496
+
497
+ | Category | Choice |
498
+ | ----------------- | -------------------------------------------------- |
499
+ | Testing Framework | pytest |
500
+ | HTTP Mocking | pytest-httpx |
501
+ | Documentation | Google-style docstrings |
502
+ | Type Signatures | Inline type annotations |
503
+ | Linting | ruff |
504
+ | Formatting | ruff format |
505
+ | HTTP Client | httpx |
506
+ | JSON Parsing | Built-in json |
507
+ | Package Manager | pip / pyproject.toml |
508
+ | Build Tool | hatchling |
509
+ | Models | dataclasses |
510
+ | Enums | str, Enum (Python 3.9+) |
511
+ | Python Version | >= 3.9 (uses `from __future__ import annotations`) |