workos 5.24.0__tar.gz → 5.26.1__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.
Files changed (157) hide show
  1. {workos-5.24.0 → workos-5.26.1}/PKG-INFO +1 -1
  2. workos-5.26.1/tests/test_organization_domains.py +136 -0
  3. {workos-5.24.0 → workos-5.26.1}/tests/test_session.py +48 -1
  4. {workos-5.24.0 → workos-5.26.1}/workos/__about__.py +1 -1
  5. {workos-5.24.0 → workos-5.26.1}/workos/_base_client.py +5 -0
  6. {workos-5.24.0 → workos-5.26.1}/workos/async_client.py +9 -0
  7. {workos-5.24.0 → workos-5.26.1}/workos/client.py +9 -0
  8. workos-5.26.1/workos/organization_domains.py +179 -0
  9. {workos-5.24.0 → workos-5.26.1}/workos/session.py +9 -3
  10. {workos-5.24.0 → workos-5.26.1}/workos/types/events/event.py +1 -1
  11. {workos-5.24.0 → workos-5.26.1}/workos/types/events/event_model.py +1 -1
  12. {workos-5.24.0 → workos-5.26.1}/workos/types/events/organization_domain_verification_failed_payload.py +1 -1
  13. workos-5.26.1/workos/types/organization_domains/__init__.py +1 -0
  14. workos-5.26.1/workos/types/organizations/__init__.py +6 -0
  15. {workos-5.24.0 → workos-5.26.1}/workos/types/organizations/organization.py +1 -1
  16. {workos-5.24.0 → workos-5.26.1}/workos/types/organizations/organization_common.py +1 -1
  17. {workos-5.24.0 → workos-5.26.1}/workos/types/webhooks/webhook.py +1 -1
  18. {workos-5.24.0 → workos-5.26.1}/workos.egg-info/PKG-INFO +1 -1
  19. {workos-5.24.0 → workos-5.26.1}/workos.egg-info/SOURCES.txt +4 -1
  20. workos-5.24.0/workos/types/organizations/__init__.py +0 -4
  21. {workos-5.24.0 → workos-5.26.1}/LICENSE +0 -0
  22. {workos-5.24.0 → workos-5.26.1}/README.md +0 -0
  23. {workos-5.24.0 → workos-5.26.1}/setup.cfg +0 -0
  24. {workos-5.24.0 → workos-5.26.1}/setup.py +0 -0
  25. {workos-5.24.0 → workos-5.26.1}/tests/test_async_http_client.py +0 -0
  26. {workos-5.24.0 → workos-5.26.1}/tests/test_audit_logs.py +0 -0
  27. {workos-5.24.0 → workos-5.26.1}/tests/test_client.py +0 -0
  28. {workos-5.24.0 → workos-5.26.1}/tests/test_directory_sync.py +0 -0
  29. {workos-5.24.0 → workos-5.26.1}/tests/test_events.py +0 -0
  30. {workos-5.24.0 → workos-5.26.1}/tests/test_fga.py +0 -0
  31. {workos-5.24.0 → workos-5.26.1}/tests/test_mfa.py +0 -0
  32. {workos-5.24.0 → workos-5.26.1}/tests/test_organizations.py +0 -0
  33. {workos-5.24.0 → workos-5.26.1}/tests/test_passwordless.py +0 -0
  34. {workos-5.24.0 → workos-5.26.1}/tests/test_portal.py +0 -0
  35. {workos-5.24.0 → workos-5.26.1}/tests/test_sso.py +0 -0
  36. {workos-5.24.0 → workos-5.26.1}/tests/test_sync_http_client.py +0 -0
  37. {workos-5.24.0 → workos-5.26.1}/tests/test_user_management.py +0 -0
  38. {workos-5.24.0 → workos-5.26.1}/tests/test_vault.py +0 -0
  39. {workos-5.24.0 → workos-5.26.1}/tests/test_webhooks.py +0 -0
  40. {workos-5.24.0 → workos-5.26.1}/tests/test_widgets.py +0 -0
  41. {workos-5.24.0 → workos-5.26.1}/workos/__init__.py +0 -0
  42. {workos-5.24.0 → workos-5.26.1}/workos/_client_configuration.py +0 -0
  43. {workos-5.24.0 → workos-5.26.1}/workos/audit_logs.py +0 -0
  44. {workos-5.24.0 → workos-5.26.1}/workos/directory_sync.py +0 -0
  45. {workos-5.24.0 → workos-5.26.1}/workos/events.py +0 -0
  46. {workos-5.24.0 → workos-5.26.1}/workos/exceptions.py +0 -0
  47. {workos-5.24.0 → workos-5.26.1}/workos/fga.py +0 -0
  48. {workos-5.24.0 → workos-5.26.1}/workos/mfa.py +0 -0
  49. {workos-5.24.0 → workos-5.26.1}/workos/organizations.py +0 -0
  50. {workos-5.24.0 → workos-5.26.1}/workos/passwordless.py +0 -0
  51. {workos-5.24.0 → workos-5.26.1}/workos/portal.py +0 -0
  52. {workos-5.24.0 → workos-5.26.1}/workos/py.typed +0 -0
  53. {workos-5.24.0 → workos-5.26.1}/workos/sso.py +0 -0
  54. {workos-5.24.0 → workos-5.26.1}/workos/types/__init__.py +0 -0
  55. {workos-5.24.0 → workos-5.26.1}/workos/types/audit_logs/__init__.py +0 -0
  56. {workos-5.24.0 → workos-5.26.1}/workos/types/audit_logs/audit_log_event.py +0 -0
  57. {workos-5.24.0 → workos-5.26.1}/workos/types/audit_logs/audit_log_event_actor.py +0 -0
  58. {workos-5.24.0 → workos-5.26.1}/workos/types/audit_logs/audit_log_event_context.py +0 -0
  59. {workos-5.24.0 → workos-5.26.1}/workos/types/audit_logs/audit_log_event_target.py +0 -0
  60. {workos-5.24.0 → workos-5.26.1}/workos/types/audit_logs/audit_log_export.py +0 -0
  61. {workos-5.24.0 → workos-5.26.1}/workos/types/audit_logs/audit_log_metadata.py +0 -0
  62. {workos-5.24.0 → workos-5.26.1}/workos/types/directory_sync/__init__.py +0 -0
  63. {workos-5.24.0 → workos-5.26.1}/workos/types/directory_sync/directory.py +0 -0
  64. {workos-5.24.0 → workos-5.26.1}/workos/types/directory_sync/directory_group.py +0 -0
  65. {workos-5.24.0 → workos-5.26.1}/workos/types/directory_sync/directory_state.py +0 -0
  66. {workos-5.24.0 → workos-5.26.1}/workos/types/directory_sync/directory_type.py +0 -0
  67. {workos-5.24.0 → workos-5.26.1}/workos/types/directory_sync/directory_user.py +0 -0
  68. {workos-5.24.0 → workos-5.26.1}/workos/types/directory_sync/list_filters.py +0 -0
  69. {workos-5.24.0 → workos-5.26.1}/workos/types/events/__init__.py +0 -0
  70. {workos-5.24.0 → workos-5.26.1}/workos/types/events/authentication_payload.py +0 -0
  71. {workos-5.24.0 → workos-5.26.1}/workos/types/events/connection_payload_with_legacy_fields.py +0 -0
  72. {workos-5.24.0 → workos-5.26.1}/workos/types/events/directory_group_membership_payload.py +0 -0
  73. {workos-5.24.0 → workos-5.26.1}/workos/types/events/directory_group_with_previous_attributes.py +0 -0
  74. {workos-5.24.0 → workos-5.26.1}/workos/types/events/directory_payload.py +0 -0
  75. {workos-5.24.0 → workos-5.26.1}/workos/types/events/directory_payload_with_legacy_fields.py +0 -0
  76. {workos-5.24.0 → workos-5.26.1}/workos/types/events/directory_user_with_previous_attributes.py +0 -0
  77. {workos-5.24.0 → workos-5.26.1}/workos/types/events/event_type.py +0 -0
  78. {workos-5.24.0 → workos-5.26.1}/workos/types/events/list_filters.py +0 -0
  79. {workos-5.24.0 → workos-5.26.1}/workos/types/events/previous_attributes.py +0 -0
  80. {workos-5.24.0 → workos-5.26.1}/workos/types/events/session_created_payload.py +0 -0
  81. {workos-5.24.0 → workos-5.26.1}/workos/types/fga/__init__.py +0 -0
  82. {workos-5.24.0 → workos-5.26.1}/workos/types/fga/authorization_resource_types.py +0 -0
  83. {workos-5.24.0 → workos-5.26.1}/workos/types/fga/authorization_resources.py +0 -0
  84. {workos-5.24.0 → workos-5.26.1}/workos/types/fga/check.py +0 -0
  85. {workos-5.24.0 → workos-5.26.1}/workos/types/fga/list_filters.py +0 -0
  86. {workos-5.24.0 → workos-5.26.1}/workos/types/fga/warnings.py +0 -0
  87. {workos-5.24.0 → workos-5.26.1}/workos/types/fga/warrant.py +0 -0
  88. {workos-5.24.0 → workos-5.26.1}/workos/types/list_resource.py +0 -0
  89. {workos-5.24.0 → workos-5.26.1}/workos/types/metadata.py +0 -0
  90. {workos-5.24.0 → workos-5.26.1}/workos/types/mfa/__init__.py +0 -0
  91. {workos-5.24.0 → workos-5.26.1}/workos/types/mfa/authentication_challenge.py +0 -0
  92. {workos-5.24.0 → workos-5.26.1}/workos/types/mfa/authentication_challenge_verification_response.py +0 -0
  93. {workos-5.24.0 → workos-5.26.1}/workos/types/mfa/authentication_factor.py +0 -0
  94. {workos-5.24.0 → workos-5.26.1}/workos/types/mfa/authentication_factor_totp_and_challenge_response.py +0 -0
  95. {workos-5.24.0 → workos-5.26.1}/workos/types/mfa/enroll_authentication_factor_type.py +0 -0
  96. {workos-5.24.0/workos/types/organizations → workos-5.26.1/workos/types/organization_domains}/organization_domain.py +0 -0
  97. {workos-5.24.0 → workos-5.26.1}/workos/types/organizations/domain_data_input.py +0 -0
  98. {workos-5.24.0 → workos-5.26.1}/workos/types/organizations/list_filters.py +0 -0
  99. {workos-5.24.0 → workos-5.26.1}/workos/types/passwordless/__init__.py +0 -0
  100. {workos-5.24.0 → workos-5.26.1}/workos/types/passwordless/passwordless_session.py +0 -0
  101. {workos-5.24.0 → workos-5.26.1}/workos/types/passwordless/passwordless_session_type.py +0 -0
  102. {workos-5.24.0 → workos-5.26.1}/workos/types/portal/__init__.py +0 -0
  103. {workos-5.24.0 → workos-5.26.1}/workos/types/portal/portal_link.py +0 -0
  104. {workos-5.24.0 → workos-5.26.1}/workos/types/portal/portal_link_intent.py +0 -0
  105. {workos-5.24.0 → workos-5.26.1}/workos/types/portal/portal_link_intent_options.py +0 -0
  106. {workos-5.24.0 → workos-5.26.1}/workos/types/roles/__init__.py +0 -0
  107. {workos-5.24.0 → workos-5.26.1}/workos/types/roles/role.py +0 -0
  108. {workos-5.24.0 → workos-5.26.1}/workos/types/sso/__init__.py +0 -0
  109. {workos-5.24.0 → workos-5.26.1}/workos/types/sso/connection.py +0 -0
  110. {workos-5.24.0 → workos-5.26.1}/workos/types/sso/connection_domain.py +0 -0
  111. {workos-5.24.0 → workos-5.26.1}/workos/types/sso/profile.py +0 -0
  112. {workos-5.24.0 → workos-5.26.1}/workos/types/sso/sso_provider_type.py +0 -0
  113. {workos-5.24.0 → workos-5.26.1}/workos/types/user_management/__init__.py +0 -0
  114. {workos-5.24.0 → workos-5.26.1}/workos/types/user_management/authenticate_with_common.py +0 -0
  115. {workos-5.24.0 → workos-5.26.1}/workos/types/user_management/authentication_response.py +0 -0
  116. {workos-5.24.0 → workos-5.26.1}/workos/types/user_management/email_verification.py +0 -0
  117. {workos-5.24.0 → workos-5.26.1}/workos/types/user_management/impersonator.py +0 -0
  118. {workos-5.24.0 → workos-5.26.1}/workos/types/user_management/invitation.py +0 -0
  119. {workos-5.24.0 → workos-5.26.1}/workos/types/user_management/list_filters.py +0 -0
  120. {workos-5.24.0 → workos-5.26.1}/workos/types/user_management/magic_auth.py +0 -0
  121. {workos-5.24.0 → workos-5.26.1}/workos/types/user_management/oauth_tokens.py +0 -0
  122. {workos-5.24.0 → workos-5.26.1}/workos/types/user_management/organization_membership.py +0 -0
  123. {workos-5.24.0 → workos-5.26.1}/workos/types/user_management/password_hash_type.py +0 -0
  124. {workos-5.24.0 → workos-5.26.1}/workos/types/user_management/password_reset.py +0 -0
  125. {workos-5.24.0 → workos-5.26.1}/workos/types/user_management/screen_hint.py +0 -0
  126. {workos-5.24.0 → workos-5.26.1}/workos/types/user_management/session.py +0 -0
  127. {workos-5.24.0 → workos-5.26.1}/workos/types/user_management/user.py +0 -0
  128. {workos-5.24.0 → workos-5.26.1}/workos/types/user_management/user_management_provider_type.py +0 -0
  129. {workos-5.24.0 → workos-5.26.1}/workos/types/vault/__init__.py +0 -0
  130. {workos-5.24.0 → workos-5.26.1}/workos/types/vault/key.py +0 -0
  131. {workos-5.24.0 → workos-5.26.1}/workos/types/vault/object.py +0 -0
  132. {workos-5.24.0 → workos-5.26.1}/workos/types/webhooks/__init__.py +0 -0
  133. {workos-5.24.0 → workos-5.26.1}/workos/types/webhooks/webhook_model.py +0 -0
  134. {workos-5.24.0 → workos-5.26.1}/workos/types/webhooks/webhook_payload.py +0 -0
  135. {workos-5.24.0 → workos-5.26.1}/workos/types/widgets/__init__.py +0 -0
  136. {workos-5.24.0 → workos-5.26.1}/workos/types/widgets/widget_scope.py +0 -0
  137. {workos-5.24.0 → workos-5.26.1}/workos/types/widgets/widget_token_response.py +0 -0
  138. {workos-5.24.0 → workos-5.26.1}/workos/types/workos_model.py +0 -0
  139. {workos-5.24.0 → workos-5.26.1}/workos/typing/__init__.py +0 -0
  140. {workos-5.24.0 → workos-5.26.1}/workos/typing/literals.py +0 -0
  141. {workos-5.24.0 → workos-5.26.1}/workos/typing/sync_or_async.py +0 -0
  142. {workos-5.24.0 → workos-5.26.1}/workos/typing/untyped_literal.py +0 -0
  143. {workos-5.24.0 → workos-5.26.1}/workos/typing/webhooks.py +0 -0
  144. {workos-5.24.0 → workos-5.26.1}/workos/user_management.py +0 -0
  145. {workos-5.24.0 → workos-5.26.1}/workos/utils/__init__.py +0 -0
  146. {workos-5.24.0 → workos-5.26.1}/workos/utils/_base_http_client.py +0 -0
  147. {workos-5.24.0 → workos-5.26.1}/workos/utils/crypto_provider.py +0 -0
  148. {workos-5.24.0 → workos-5.26.1}/workos/utils/http_client.py +0 -0
  149. {workos-5.24.0 → workos-5.26.1}/workos/utils/pagination_order.py +0 -0
  150. {workos-5.24.0 → workos-5.26.1}/workos/utils/request_helper.py +0 -0
  151. {workos-5.24.0 → workos-5.26.1}/workos/vault.py +0 -0
  152. {workos-5.24.0 → workos-5.26.1}/workos/webhooks.py +0 -0
  153. {workos-5.24.0 → workos-5.26.1}/workos/widgets.py +0 -0
  154. {workos-5.24.0 → workos-5.26.1}/workos.egg-info/dependency_links.txt +0 -0
  155. {workos-5.24.0 → workos-5.26.1}/workos.egg-info/not-zip-safe +0 -0
  156. {workos-5.24.0 → workos-5.26.1}/workos.egg-info/requires.txt +0 -0
  157. {workos-5.24.0 → workos-5.26.1}/workos.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: workos
3
- Version: 5.24.0
3
+ Version: 5.26.1
4
4
  Summary: WorkOS Python Client
5
5
  Home-page: https://github.com/workos-inc/workos-python
6
6
  Author: WorkOS
@@ -0,0 +1,136 @@
1
+ from typing import Union
2
+ import pytest
3
+ from tests.utils.syncify import syncify
4
+ from workos.organization_domains import AsyncOrganizationDomains, OrganizationDomains
5
+
6
+
7
+ @pytest.mark.sync_and_async(OrganizationDomains, AsyncOrganizationDomains)
8
+ class TestOrganizationDomains:
9
+ @pytest.fixture(autouse=True)
10
+ def setup(
11
+ self, module_instance: Union[OrganizationDomains, AsyncOrganizationDomains]
12
+ ):
13
+ self.http_client = module_instance._http_client
14
+ self.organization_domains = module_instance
15
+
16
+ @pytest.fixture
17
+ def mock_organization_domain(self):
18
+ return {
19
+ "object": "organization_domain",
20
+ "id": "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8",
21
+ "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T",
22
+ "domain": "example.com",
23
+ "state": "pending",
24
+ "verification_strategy": "dns",
25
+ "verification_token": "workos_example_verification_token_12345",
26
+ "verification_prefix": "_workos-challenge",
27
+ "created_at": "2023-01-01T12:00:00.000Z",
28
+ "updated_at": "2023-01-01T12:00:00.000Z",
29
+ }
30
+
31
+ @pytest.fixture
32
+ def mock_organization_domain_verified(self):
33
+ return {
34
+ "object": "organization_domain",
35
+ "id": "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8",
36
+ "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T",
37
+ "domain": "example.com",
38
+ "state": "verified",
39
+ "verification_strategy": "dns",
40
+ "verification_token": "workos_example_verification_token_12345",
41
+ "verification_prefix": "_workos-challenge",
42
+ "created_at": "2023-01-01T12:00:00.000Z",
43
+ "updated_at": "2023-01-01T12:00:00.000Z",
44
+ }
45
+
46
+ def test_get_organization_domain(
47
+ self, capture_and_mock_http_client_request, mock_organization_domain
48
+ ):
49
+ request_kwargs = capture_and_mock_http_client_request(
50
+ self.http_client,
51
+ mock_organization_domain,
52
+ 200,
53
+ )
54
+
55
+ organization_domain = syncify(
56
+ self.organization_domains.get_organization_domain(
57
+ organization_domain_id="org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8"
58
+ )
59
+ )
60
+
61
+ assert request_kwargs["url"].endswith(
62
+ "/organization_domains/org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8"
63
+ )
64
+ assert request_kwargs["method"] == "get"
65
+ assert organization_domain.id == "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8"
66
+ assert organization_domain.domain == "example.com"
67
+ assert organization_domain.state == "pending"
68
+ assert organization_domain.verification_strategy == "dns"
69
+
70
+ def test_create_organization_domain(
71
+ self, capture_and_mock_http_client_request, mock_organization_domain
72
+ ):
73
+ request_kwargs = capture_and_mock_http_client_request(
74
+ self.http_client,
75
+ mock_organization_domain,
76
+ 201,
77
+ )
78
+
79
+ organization_domain = syncify(
80
+ self.organization_domains.create_organization_domain(
81
+ organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T",
82
+ domain="example.com",
83
+ )
84
+ )
85
+
86
+ assert request_kwargs["url"].endswith("/organization_domains")
87
+ assert request_kwargs["method"] == "post"
88
+ assert request_kwargs["json"] == {
89
+ "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T",
90
+ "domain": "example.com",
91
+ }
92
+ assert organization_domain.id == "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8"
93
+ assert organization_domain.domain == "example.com"
94
+ assert organization_domain.organization_id == "org_01EHT88Z8J8795GZNQ4ZP1J81T"
95
+
96
+ def test_verify_organization_domain(
97
+ self, capture_and_mock_http_client_request, mock_organization_domain_verified
98
+ ):
99
+ request_kwargs = capture_and_mock_http_client_request(
100
+ self.http_client,
101
+ mock_organization_domain_verified,
102
+ 200,
103
+ )
104
+
105
+ organization_domain = syncify(
106
+ self.organization_domains.verify_organization_domain(
107
+ organization_domain_id="org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8"
108
+ )
109
+ )
110
+
111
+ assert request_kwargs["url"].endswith(
112
+ "/organization_domains/org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8/verify"
113
+ )
114
+ assert request_kwargs["method"] == "post"
115
+ assert organization_domain.id == "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8"
116
+ assert organization_domain.state == "verified"
117
+
118
+ def test_delete_organization_domain(self, capture_and_mock_http_client_request):
119
+ request_kwargs = capture_and_mock_http_client_request(
120
+ self.http_client,
121
+ None,
122
+ 204,
123
+ headers={"content-type": "text/plain; charset=utf-8"},
124
+ )
125
+
126
+ response = syncify(
127
+ self.organization_domains.delete_organization_domain(
128
+ organization_domain_id="org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8"
129
+ )
130
+ )
131
+
132
+ assert request_kwargs["url"].endswith(
133
+ "/organization_domains/org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8"
134
+ )
135
+ assert request_kwargs["method"] == "delete"
136
+ assert response is None
@@ -2,9 +2,10 @@ import pytest
2
2
  from unittest.mock import AsyncMock, Mock, patch
3
3
  import jwt
4
4
  from datetime import datetime, timezone
5
+ import concurrent.futures
5
6
 
6
7
  from tests.conftest import with_jwks_mock
7
- from workos.session import AsyncSession, Session
8
+ from workos.session import AsyncSession, Session, _get_jwks_client
8
9
  from workos.types.user_management.authentication_response import (
9
10
  RefreshTokenAuthenticationResponse,
10
11
  )
@@ -20,6 +21,12 @@ from cryptography.hazmat.primitives.asymmetric import rsa
20
21
 
21
22
 
22
23
  class SessionFixtures:
24
+ @pytest.fixture(autouse=True)
25
+ def clear_jwks_cache(self):
26
+ _get_jwks_client.cache_clear()
27
+ yield
28
+ _get_jwks_client.cache_clear()
29
+
23
30
  @pytest.fixture
24
31
  def session_constants(self):
25
32
  # Generate RSA key pair for testing
@@ -491,3 +498,43 @@ class TestAsyncSession(SessionFixtures):
491
498
  response = await session.refresh()
492
499
 
493
500
  assert isinstance(response, RefreshWithSessionCookieSuccessResponse)
501
+
502
+
503
+ class TestJWKSCaching:
504
+ def test_jwks_client_caching_same_url(self):
505
+ url = "https://api.workos.com/sso/jwks/test"
506
+
507
+ client1 = _get_jwks_client(url)
508
+ client2 = _get_jwks_client(url)
509
+
510
+ # Should be the exact same instance
511
+ assert client1 is client2
512
+ assert id(client1) == id(client2)
513
+
514
+ def test_jwks_client_caching_different_urls(self):
515
+ url1 = "https://api.workos.com/sso/jwks/client1"
516
+ url2 = "https://api.workos.com/sso/jwks/client2"
517
+
518
+ client1 = _get_jwks_client(url1)
519
+ client2 = _get_jwks_client(url2)
520
+
521
+ # Should be different instances
522
+ assert client1 is not client2
523
+ assert id(client1) != id(client2)
524
+
525
+ def test_jwks_cache_thread_safety(self):
526
+ url = "https://api.workos.com/sso/jwks/thread_test"
527
+ clients = []
528
+
529
+ def get_client():
530
+ return _get_jwks_client(url)
531
+
532
+ with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
533
+ futures = [executor.submit(get_client) for _ in range(10)]
534
+ clients = [future.result() for future in futures]
535
+
536
+ first_client = clients[0]
537
+ for client in clients[1:]:
538
+ assert (
539
+ client is first_client
540
+ ), "All concurrent calls should return the same instance"
@@ -12,7 +12,7 @@ __package_name__ = "workos"
12
12
 
13
13
  __package_url__ = "https://github.com/workos-inc/workos-python"
14
14
 
15
- __version__ = "5.24.0"
15
+ __version__ = "5.26.1"
16
16
 
17
17
  __author__ = "WorkOS"
18
18
 
@@ -11,6 +11,7 @@ from workos.directory_sync import DirectorySyncModule
11
11
  from workos.events import EventsModule
12
12
  from workos.mfa import MFAModule
13
13
  from workos.organizations import OrganizationsModule
14
+ from workos.organization_domains import OrganizationDomainsModule
14
15
  from workos.passwordless import PasswordlessModule
15
16
  from workos.portal import PortalModule
16
17
  from workos.sso import SSOModule
@@ -88,6 +89,10 @@ class BaseClient(ClientConfiguration):
88
89
  @abstractmethod
89
90
  def organizations(self) -> OrganizationsModule: ...
90
91
 
92
+ @property
93
+ @abstractmethod
94
+ def organization_domains(self) -> OrganizationDomainsModule: ...
95
+
91
96
  @property
92
97
  @abstractmethod
93
98
  def passwordless(self) -> PasswordlessModule: ...
@@ -7,6 +7,7 @@ from workos.events import AsyncEvents
7
7
  from workos.fga import FGAModule
8
8
  from workos.mfa import MFAModule
9
9
  from workos.organizations import AsyncOrganizations
10
+ from workos.organization_domains import AsyncOrganizationDomains
10
11
  from workos.passwordless import PasswordlessModule
11
12
  from workos.portal import PortalModule
12
13
  from workos.sso import AsyncSSO
@@ -80,6 +81,14 @@ class AsyncClient(BaseClient):
80
81
  self._organizations = AsyncOrganizations(self._http_client)
81
82
  return self._organizations
82
83
 
84
+ @property
85
+ def organization_domains(self) -> AsyncOrganizationDomains:
86
+ if not getattr(self, "_organization_domains", None):
87
+ self._organization_domains = AsyncOrganizationDomains(
88
+ http_client=self._http_client, client_configuration=self
89
+ )
90
+ return self._organization_domains
91
+
83
92
  @property
84
93
  def passwordless(self) -> PasswordlessModule:
85
94
  raise NotImplementedError(
@@ -5,6 +5,7 @@ from workos.audit_logs import AuditLogs
5
5
  from workos.directory_sync import DirectorySync
6
6
  from workos.fga import FGA
7
7
  from workos.organizations import Organizations
8
+ from workos.organization_domains import OrganizationDomains
8
9
  from workos.passwordless import Passwordless
9
10
  from workos.portal import Portal
10
11
  from workos.sso import SSO
@@ -80,6 +81,14 @@ class SyncClient(BaseClient):
80
81
  self._organizations = Organizations(self._http_client)
81
82
  return self._organizations
82
83
 
84
+ @property
85
+ def organization_domains(self) -> OrganizationDomains:
86
+ if not getattr(self, "_organization_domains", None):
87
+ self._organization_domains = OrganizationDomains(
88
+ http_client=self._http_client, client_configuration=self
89
+ )
90
+ return self._organization_domains
91
+
83
92
  @property
84
93
  def passwordless(self) -> Passwordless:
85
94
  if not getattr(self, "_passwordless", None):
@@ -0,0 +1,179 @@
1
+ from typing import Protocol
2
+ from workos._client_configuration import ClientConfiguration
3
+ from workos.types.organization_domains import OrganizationDomain
4
+ from workos.typing.sync_or_async import SyncOrAsync
5
+ from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient
6
+ from workos.utils.request_helper import (
7
+ REQUEST_METHOD_DELETE,
8
+ REQUEST_METHOD_GET,
9
+ REQUEST_METHOD_POST,
10
+ )
11
+
12
+
13
+ class OrganizationDomainsModule(Protocol):
14
+ """Offers methods for managing organization domains."""
15
+
16
+ _client_configuration: ClientConfiguration
17
+
18
+ def get_organization_domain(
19
+ self, organization_domain_id: str
20
+ ) -> SyncOrAsync[OrganizationDomain]:
21
+ """Gets a single Organization Domain
22
+
23
+ Args:
24
+ organization_domain_id (str): Organization Domain unique identifier
25
+
26
+ Returns:
27
+ OrganizationDomain: Organization Domain response from WorkOS
28
+ """
29
+ ...
30
+
31
+ def create_organization_domain(
32
+ self,
33
+ organization_id: str,
34
+ domain: str,
35
+ ) -> SyncOrAsync[OrganizationDomain]:
36
+ """Creates an Organization Domain
37
+
38
+ Args:
39
+ organization_id (str): Organization unique identifier
40
+ domain (str): Domain to be added to the organization
41
+
42
+ Returns:
43
+ OrganizationDomain: Organization Domain response from WorkOS
44
+ """
45
+ ...
46
+
47
+ def verify_organization_domain(
48
+ self, organization_domain_id: str
49
+ ) -> SyncOrAsync[OrganizationDomain]:
50
+ """Verifies an Organization Domain
51
+
52
+ Args:
53
+ organization_domain_id (str): Organization Domain unique identifier
54
+
55
+ Returns:
56
+ OrganizationDomain: Organization Domain response from WorkOS
57
+ """
58
+ ...
59
+
60
+ def delete_organization_domain(
61
+ self, organization_domain_id: str
62
+ ) -> SyncOrAsync[None]:
63
+ """Deletes a single Organization Domain
64
+
65
+ Args:
66
+ organization_domain_id (str): Organization Domain unique identifier
67
+
68
+ Returns:
69
+ None
70
+ """
71
+ ...
72
+
73
+
74
+ class OrganizationDomains:
75
+ """Offers methods for managing organization domains."""
76
+
77
+ _http_client: SyncHTTPClient
78
+ _client_configuration: ClientConfiguration
79
+
80
+ def __init__(
81
+ self,
82
+ http_client: SyncHTTPClient,
83
+ client_configuration: ClientConfiguration,
84
+ ):
85
+ self._http_client = http_client
86
+ self._client_configuration = client_configuration
87
+
88
+ def get_organization_domain(
89
+ self, organization_domain_id: str
90
+ ) -> OrganizationDomain:
91
+ response = self._http_client.request(
92
+ f"organization_domains/{organization_domain_id}",
93
+ method=REQUEST_METHOD_GET,
94
+ )
95
+
96
+ return OrganizationDomain.model_validate(response)
97
+
98
+ def create_organization_domain(
99
+ self,
100
+ organization_id: str,
101
+ domain: str,
102
+ ) -> OrganizationDomain:
103
+ response = self._http_client.request(
104
+ "organization_domains",
105
+ method=REQUEST_METHOD_POST,
106
+ json={"organization_id": organization_id, "domain": domain},
107
+ )
108
+
109
+ return OrganizationDomain.model_validate(response)
110
+
111
+ def verify_organization_domain(
112
+ self, organization_domain_id: str
113
+ ) -> OrganizationDomain:
114
+ response = self._http_client.request(
115
+ f"organization_domains/{organization_domain_id}/verify",
116
+ method=REQUEST_METHOD_POST,
117
+ )
118
+
119
+ return OrganizationDomain.model_validate(response)
120
+
121
+ def delete_organization_domain(self, organization_domain_id: str) -> None:
122
+ self._http_client.request(
123
+ f"organization_domains/{organization_domain_id}",
124
+ method=REQUEST_METHOD_DELETE,
125
+ )
126
+
127
+
128
+ class AsyncOrganizationDomains:
129
+ """Offers async methods for managing organization domains."""
130
+
131
+ _http_client: AsyncHTTPClient
132
+ _client_configuration: ClientConfiguration
133
+
134
+ def __init__(
135
+ self,
136
+ http_client: AsyncHTTPClient,
137
+ client_configuration: ClientConfiguration,
138
+ ):
139
+ self._http_client = http_client
140
+ self._client_configuration = client_configuration
141
+
142
+ async def get_organization_domain(
143
+ self, organization_domain_id: str
144
+ ) -> OrganizationDomain:
145
+ response = await self._http_client.request(
146
+ f"organization_domains/{organization_domain_id}",
147
+ method=REQUEST_METHOD_GET,
148
+ )
149
+
150
+ return OrganizationDomain.model_validate(response)
151
+
152
+ async def create_organization_domain(
153
+ self,
154
+ organization_id: str,
155
+ domain: str,
156
+ ) -> OrganizationDomain:
157
+ response = await self._http_client.request(
158
+ "organization_domains",
159
+ method=REQUEST_METHOD_POST,
160
+ json={"organization_id": organization_id, "domain": domain},
161
+ )
162
+
163
+ return OrganizationDomain.model_validate(response)
164
+
165
+ async def verify_organization_domain(
166
+ self, organization_domain_id: str
167
+ ) -> OrganizationDomain:
168
+ response = await self._http_client.request(
169
+ f"organization_domains/{organization_domain_id}/verify",
170
+ method=REQUEST_METHOD_POST,
171
+ )
172
+
173
+ return OrganizationDomain.model_validate(response)
174
+
175
+ async def delete_organization_domain(self, organization_domain_id: str) -> None:
176
+ await self._http_client.request(
177
+ f"organization_domains/{organization_domain_id}",
178
+ method=REQUEST_METHOD_DELETE,
179
+ )
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
  from typing import TYPE_CHECKING, List, Protocol
3
3
 
4
+ from functools import lru_cache
4
5
  import json
5
6
  from typing import Any, Dict, Optional, Union, cast
6
7
  import jwt
@@ -21,6 +22,11 @@ if TYPE_CHECKING:
21
22
  from workos.user_management import AsyncUserManagement, UserManagement
22
23
 
23
24
 
25
+ @lru_cache(maxsize=None)
26
+ def _get_jwks_client(jwks_url: str) -> PyJWKClient:
27
+ return PyJWKClient(jwks_url)
28
+
29
+
24
30
  class SessionModule(Protocol):
25
31
  user_management: "UserManagementModule"
26
32
  client_id: str
@@ -46,7 +52,7 @@ class SessionModule(Protocol):
46
52
  self.session_data = session_data
47
53
  self.cookie_password = cookie_password
48
54
 
49
- self.jwks = PyJWKClient(self.user_management.get_jwks_url())
55
+ self.jwks = _get_jwks_client(self.user_management.get_jwks_url())
50
56
 
51
57
  # Algorithms are hardcoded for security reasons. See https://pyjwt.readthedocs.io/en/stable/algorithms.html#specifying-an-algorithm
52
58
  self.jwk_algorithms = ["RS256"]
@@ -164,7 +170,7 @@ class Session(SessionModule):
164
170
  self.session_data = session_data
165
171
  self.cookie_password = cookie_password
166
172
 
167
- self.jwks = PyJWKClient(self.user_management.get_jwks_url())
173
+ self.jwks = _get_jwks_client(self.user_management.get_jwks_url())
168
174
 
169
175
  # Algorithms are hardcoded for security reasons. See https://pyjwt.readthedocs.io/en/stable/algorithms.html#specifying-an-algorithm
170
176
  self.jwk_algorithms = ["RS256"]
@@ -254,7 +260,7 @@ class AsyncSession(SessionModule):
254
260
  self.session_data = session_data
255
261
  self.cookie_password = cookie_password
256
262
 
257
- self.jwks = PyJWKClient(self.user_management.get_jwks_url())
263
+ self.jwks = _get_jwks_client(self.user_management.get_jwks_url())
258
264
 
259
265
  # Algorithms are hardcoded for security reasons. See https://pyjwt.readthedocs.io/en/stable/algorithms.html#specifying-an-algorithm
260
266
  self.jwk_algorithms = ["RS256"]
@@ -38,7 +38,7 @@ from workos.types.events.organization_domain_verification_failed_payload import
38
38
  )
39
39
  from workos.types.events.session_created_payload import SessionCreatedPayload
40
40
  from workos.types.organizations.organization_common import OrganizationCommon
41
- from workos.types.organizations.organization_domain import OrganizationDomain
41
+ from workos.types.organization_domains import OrganizationDomain
42
42
  from workos.types.roles.role import EventRole
43
43
  from workos.types.sso.connection import Connection
44
44
  from workos.types.user_management.email_verification import (
@@ -37,7 +37,7 @@ from workos.types.events.organization_domain_verification_failed_payload import
37
37
  )
38
38
  from workos.types.events.session_created_payload import SessionCreatedPayload
39
39
  from workos.types.organizations.organization_common import OrganizationCommon
40
- from workos.types.organizations.organization_domain import OrganizationDomain
40
+ from workos.types.organization_domains import OrganizationDomain
41
41
  from workos.types.roles.role import EventRole
42
42
  from workos.types.sso.connection import Connection
43
43
  from workos.types.user_management.email_verification import (
@@ -1,6 +1,6 @@
1
1
  from typing import Literal
2
2
  from workos.types.workos_model import WorkOSModel
3
- from workos.types.organizations.organization_domain import OrganizationDomain
3
+ from workos.types.organization_domains import OrganizationDomain
4
4
  from workos.typing.literals import LiteralOrUntyped
5
5
 
6
6
 
@@ -0,0 +1 @@
1
+ from .organization_domain import *
@@ -0,0 +1,6 @@
1
+ from .domain_data_input import *
2
+ from .organization_common import *
3
+ from .organization import *
4
+
5
+ # re-exported for backwards compatibility, can be removed after version 6.0.0
6
+ from workos.types.organization_domains import OrganizationDomain
@@ -2,7 +2,7 @@ from dataclasses import field
2
2
  from typing import Optional, Sequence
3
3
  from workos.types.metadata import Metadata
4
4
  from workos.types.organizations.organization_common import OrganizationCommon
5
- from workos.types.organizations.organization_domain import OrganizationDomain
5
+ from workos.types.organization_domains import OrganizationDomain
6
6
 
7
7
 
8
8
  class Organization(OrganizationCommon):
@@ -1,6 +1,6 @@
1
1
  from typing import Literal, Sequence
2
2
  from workos.types.workos_model import WorkOSModel
3
- from workos.types.organizations.organization_domain import OrganizationDomain
3
+ from workos.types.organization_domains import OrganizationDomain
4
4
 
5
5
 
6
6
  class OrganizationCommon(WorkOSModel):
@@ -38,7 +38,7 @@ from workos.types.events.organization_domain_verification_failed_payload import
38
38
  )
39
39
  from workos.types.events.session_created_payload import SessionCreatedPayload
40
40
  from workos.types.organizations.organization_common import OrganizationCommon
41
- from workos.types.organizations.organization_domain import OrganizationDomain
41
+ from workos.types.organization_domains import OrganizationDomain
42
42
  from workos.types.roles.role import EventRole
43
43
  from workos.types.sso.connection import Connection
44
44
  from workos.types.user_management.email_verification import (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: workos
3
- Version: 5.24.0
3
+ Version: 5.26.1
4
4
  Summary: WorkOS Python Client
5
5
  Home-page: https://github.com/workos-inc/workos-python
6
6
  Author: WorkOS
@@ -8,6 +8,7 @@ tests/test_directory_sync.py
8
8
  tests/test_events.py
9
9
  tests/test_fga.py
10
10
  tests/test_mfa.py
11
+ tests/test_organization_domains.py
11
12
  tests/test_organizations.py
12
13
  tests/test_passwordless.py
13
14
  tests/test_portal.py
@@ -30,6 +31,7 @@ workos/events.py
30
31
  workos/exceptions.py
31
32
  workos/fga.py
32
33
  workos/mfa.py
34
+ workos/organization_domains.py
33
35
  workos/organizations.py
34
36
  workos/passwordless.py
35
37
  workos/portal.py
@@ -92,12 +94,13 @@ workos/types/mfa/authentication_challenge_verification_response.py
92
94
  workos/types/mfa/authentication_factor.py
93
95
  workos/types/mfa/authentication_factor_totp_and_challenge_response.py
94
96
  workos/types/mfa/enroll_authentication_factor_type.py
97
+ workos/types/organization_domains/__init__.py
98
+ workos/types/organization_domains/organization_domain.py
95
99
  workos/types/organizations/__init__.py
96
100
  workos/types/organizations/domain_data_input.py
97
101
  workos/types/organizations/list_filters.py
98
102
  workos/types/organizations/organization.py
99
103
  workos/types/organizations/organization_common.py
100
- workos/types/organizations/organization_domain.py
101
104
  workos/types/passwordless/__init__.py
102
105
  workos/types/passwordless/passwordless_session.py
103
106
  workos/types/passwordless/passwordless_session_type.py
@@ -1,4 +0,0 @@
1
- from .domain_data_input import *
2
- from .organization_common import *
3
- from .organization_domain import *
4
- from .organization import *
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes