From e24084e2456979ea1f8edffc436bbf99a1db34a6 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Thu, 26 Mar 2026 23:45:32 -0400 Subject: [PATCH 01/26] style: fix ruff formatting in test_session.py Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/test_session.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/tests/test_session.py b/tests/test_session.py index a51b38ba..9894b9b7 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -249,12 +249,14 @@ def test_authenticate_success(self, session_constants, mock_user_management): "feature_flags": ["flag1", "flag2"], } - with patch.object(Session, "unseal_data", return_value=mock_session), patch( - "jwt.decode", return_value=mock_jwt_payload - ), patch.object( - session.jwks, - "get_signing_key_from_jwt", - return_value=Mock(key=session_constants["PUBLIC_KEY"]), + with ( + patch.object(Session, "unseal_data", return_value=mock_session), + patch("jwt.decode", return_value=mock_jwt_payload), + patch.object( + session.jwks, + "get_signing_key_from_jwt", + return_value=Mock(key=session_constants["PUBLIC_KEY"]), + ), ): response = session.authenticate() @@ -319,12 +321,14 @@ def test_authenticate_success_with_roles( "feature_flags": ["flag1", "flag2"], } - with patch.object(Session, "unseal_data", return_value=mock_session), patch( - "jwt.decode", return_value=mock_jwt_payload - ), patch.object( - session.jwks, - "get_signing_key_from_jwt", - return_value=Mock(key=session_constants["PUBLIC_KEY"]), + with ( + patch.object(Session, "unseal_data", return_value=mock_session), + patch("jwt.decode", return_value=mock_jwt_payload), + patch.object( + session.jwks, + "get_signing_key_from_jwt", + return_value=Mock(key=session_constants["PUBLIC_KEY"]), + ), ): response = session.authenticate() From 6033b937efe7bdd62d683bf5da556048a6ca947a Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Fri, 27 Mar 2026 01:10:22 -0400 Subject: [PATCH 02/26] chore: switch type checker from mypy to pyright strict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace mypy with pyright as the primary type checker. pyright is faster, catches more real bugs (better type narrowing, generic inference, union handling), and is the industry standard for production Python SDKs (used by OpenAI, Anthropic, Stripe). - Replace mypy with pyright in noxfile.py (typecheck + ci sessions) - Swap mypy for pyright in type_check dependency group - Remove [tool.mypy] config section - Add [tool.pyright] with typeCheckingMode = "strict" - Update uv.lock The generated SDK passes pyright strict with 0 errors across 374 files. This also addresses #600 (SsoProviderType enum removal) — the emitter generates all spec enums as str/Enum classes, restoring attribute access (e.g., EnumName.VALUE) that was lost when v5 switched to Literal type aliases. Co-Authored-By: Claude Opus 4.6 (1M context) --- noxfile.py | 6 ++--- pyproject.toml | 19 +++++--------- uv.lock | 67 ++++++++++++++++---------------------------------- 3 files changed, 30 insertions(+), 62 deletions(-) diff --git a/noxfile.py b/noxfile.py index bf0e2b3a..3c46b8c6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -56,8 +56,8 @@ def format_fix(s: nox.Session) -> None: @session(uv_groups=["type_check"]) def typecheck(s: nox.Session) -> None: - """Run type checking with mypy.""" - s.run("mypy") + """Run type checking with pyright.""" + s.run("pyright") @session(uv_groups=["test", "lint", "type_check"]) @@ -68,5 +68,5 @@ def ci(s: nox.Session) -> None: """ s.run("ruff", "format", "--check", ".") s.run("ruff", "check", ".") - s.run("mypy") + s.run("pyright") s.run("pytest") diff --git a/pyproject.toml b/pyproject.toml index 73a3079c..51e76164 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dev = [ { include-group = "lint" }, { include-group = "type_check" }, { include-group = "nox" }, + "pyright>=1.1.408", ] test = [ "pytest==8.3.4", @@ -33,25 +34,13 @@ test = [ "six==1.17.0", ] lint = ["ruff==0.14.5"] -type_check = ["mypy==1.14.1"] +type_check = ["pyright>=1.1.408"] nox = [ "nox>=2024.10.9", "nox-uv>=0.7.0", ] -[tool.mypy] -packages = "workos" -warn_return_any = true -warn_unused_configs = true -warn_unreachable = true -warn_redundant_casts = true -warn_no_return = true -warn_unused_ignores = true -implicit_reexport = true -strict_equality = true -strict = true - [tool.ruff.lint.per-file-ignores] "*/__init__.py" = ["F401", "F403"] @@ -62,6 +51,10 @@ max-complexity = 10 source-include = ["py.typed"] source-exclude = ["tests*"] +[tool.pyright] +typeCheckingMode = "strict" +include = ["workos"] + [build-system] requires = ["uv_build>=0.8.15,<0.9.0"] build-backend = "uv_build" diff --git a/uv.lock b/uv.lock index e36764f0..7feb357c 100644 --- a/uv.lock +++ b/uv.lock @@ -428,50 +428,12 @@ wheels = [ ] [[package]] -name = "mypy" -version = "1.14.1" +name = "nodeenv" +version = "1.10.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload-time = "2024-12-30T16:37:22.435Z" }, - { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload-time = "2024-12-30T16:37:53.526Z" }, - { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload-time = "2024-12-30T16:37:50.332Z" }, - { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload-time = "2024-12-30T16:37:29.96Z" }, - { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload-time = "2024-12-30T16:38:05.037Z" }, - { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload-time = "2024-12-30T16:37:19.726Z" }, - { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload-time = "2024-12-30T16:37:11.533Z" }, - { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload-time = "2024-12-30T16:37:40.724Z" }, - { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload-time = "2024-12-30T16:36:58.73Z" }, - { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload-time = "2024-12-30T16:37:03.741Z" }, - { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload-time = "2024-12-30T16:37:57.948Z" }, - { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload-time = "2024-12-30T16:37:33.428Z" }, - { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload-time = "2024-12-30T16:38:02.211Z" }, - { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload-time = "2024-12-30T16:37:46.131Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload-time = "2024-12-30T16:37:43.534Z" }, - { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload-time = "2024-12-30T16:37:36.249Z" }, - { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload-time = "2024-12-30T16:37:06.743Z" }, - { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload-time = "2024-12-30T16:37:15.02Z" }, - { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload-time = "2024-12-30T16:37:25.144Z" }, - { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload-time = "2024-12-30T16:38:08.634Z" }, - { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload-time = "2024-12-30T16:38:12.132Z" }, - { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload-time = "2024-12-30T16:38:17.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload-time = "2024-12-30T16:38:32.839Z" }, - { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload-time = "2024-12-30T16:38:20.828Z" }, - { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload-time = "2024-12-30T16:38:42.021Z" }, -] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] [[package]] @@ -686,6 +648,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, ] +[[package]] +name = "pyright" +version = "1.1.408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, +] + [[package]] name = "pytest" version = "8.3.4" @@ -870,9 +845,9 @@ dependencies = [ [package.dev-dependencies] dev = [ - { name = "mypy" }, { name = "nox" }, { name = "nox-uv" }, + { name = "pyright" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -893,7 +868,7 @@ test = [ { name = "six" }, ] type-check = [ - { name = "mypy" }, + { name = "pyright" }, ] [package.metadata] @@ -906,9 +881,9 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "mypy", specifier = "==1.14.1" }, { name = "nox", specifier = ">=2024.10.9" }, { name = "nox-uv", specifier = ">=0.7.0" }, + { name = "pyright", specifier = ">=1.1.408" }, { name = "pytest", specifier = "==8.3.4" }, { name = "pytest-asyncio", specifier = "==0.23.8" }, { name = "pytest-cov", specifier = "==5.0.0" }, @@ -926,4 +901,4 @@ test = [ { name = "pytest-cov", specifier = "==5.0.0" }, { name = "six", specifier = "==1.17.0" }, ] -type-check = [{ name = "mypy", specifier = "==1.14.1" }] +type-check = [{ name = "pyright", specifier = ">=1.1.408" }] From 39e7c78f087a35a43bdcb2b3f28099f90f7cb0bd Mon Sep 17 00:00:00 2001 From: Garen Torikian Date: Fri, 27 Mar 2026 14:53:57 -0400 Subject: [PATCH 03/26] chore(deps): update dev dependencies (ruff, nox, nox-uv) (#601) --- pyproject.toml | 14 ++----- tests/test_sso.py | 2 +- tests/test_user_management.py | 2 +- uv.lock | 73 +++++++++++++++++------------------ 4 files changed, 41 insertions(+), 50 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 51e76164..0b99b418 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,18 +27,10 @@ dev = [ { include-group = "nox" }, "pyright>=1.1.408", ] -test = [ - "pytest==8.3.4", - "pytest-asyncio==0.23.8", - "pytest-cov==5.0.0", - "six==1.17.0", -] +test = ["pytest~=8.3", "pytest-asyncio~=1.3", "pytest-cov~=7.0"] lint = ["ruff==0.14.5"] -type_check = ["pyright>=1.1.408"] -nox = [ - "nox>=2024.10.9", - "nox-uv>=0.7.0", -] +type_check = ["pyright~=1.1"] +nox = ["nox~=2026.2", "nox-uv~=0.7"] [tool.ruff.lint.per-file-ignores] diff --git a/tests/test_sso.py b/tests/test_sso.py index ec2a557a..4abc585d 100644 --- a/tests/test_sso.py +++ b/tests/test_sso.py @@ -1,6 +1,6 @@ import json from typing import Union -from six.moves.urllib.parse import parse_qsl, urlparse +from urllib.parse import parse_qsl, urlparse import pytest from tests.types.test_auto_pagination_function import TestAutoPaginationFunction from tests.utils.fixtures.mock_profile import MockProfile diff --git a/tests/test_user_management.py b/tests/test_user_management.py index e4dd5b63..19069abd 100644 --- a/tests/test_user_management.py +++ b/tests/test_user_management.py @@ -1,7 +1,7 @@ import json from typing import Union -from six.moves.urllib.parse import parse_qsl, urlparse +from urllib.parse import parse_qsl, urlparse import pytest from tests.utils.fixtures.mock_auth_factor_totp import MockAuthenticationFactorTotp diff --git a/uv.lock b/uv.lock index 7feb357c..5fab0e13 100644 --- a/uv.lock +++ b/uv.lock @@ -44,6 +44,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + [[package]] name = "certifi" version = "2025.11.12" @@ -438,7 +447,7 @@ wheels = [ [[package]] name = "nox" -version = "2025.11.12" +version = "2026.2.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete" }, @@ -450,21 +459,21 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/a8/e169497599266d176832e2232c08557ffba97eef87bf8a18f9f918e0c6aa/nox-2025.11.12.tar.gz", hash = "sha256:3d317f9e61f49d6bde39cf2f59695bb4e1722960457eee3ae19dacfe03c07259", size = 4030561, upload-time = "2025-11-12T18:39:03.319Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/8e/55a9679b31f1efc48facedd2448eb53c7f1e647fb592aa1403c9dd7a4590/nox-2026.2.9.tar.gz", hash = "sha256:1bc8a202ee8cd69be7aaada63b2a7019126899a06fc930a7aee75585bf8ee41b", size = 4031165, upload-time = "2026-02-10T04:38:58.878Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/34/434c594e0125a16b05a7bedaea33e63c90abbfbe47e5729a735a8a8a90ea/nox-2025.11.12-py3-none-any.whl", hash = "sha256:707171f9f63bc685da9d00edd8c2ceec8405b8e38b5fb4e46114a860070ef0ff", size = 74447, upload-time = "2025-11-12T18:39:01.575Z" }, + { url = "https://files.pythonhosted.org/packages/8d/58/0d5e5a044f1868bdc45f38afdc2d90ff9867ce398b4e8fa9e666bfc9bfba/nox-2026.2.9-py3-none-any.whl", hash = "sha256:1b7143bc8ecdf25f2353201326152c5303ae4ae56ca097b1fb6179ad75164c47", size = 74615, upload-time = "2026-02-10T04:38:57.266Z" }, ] [[package]] name = "nox-uv" -version = "0.7.0" +version = "0.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nox" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/a2/be2f4d0fd1632213eb5962c8e5ee27def06305403786e21fc3179c7e26e6/nox_uv-0.7.0.tar.gz", hash = "sha256:60ac21c16650f05ebb520d737cc2e838c7a49be2ad1dbcd7165ffd3675560991", size = 5077, upload-time = "2026-01-03T19:54:59.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/e8/670919c513c22f4bf1656d84dd99a9ad1a5eaaeadf2457bab3efeeac14e0/nox_uv-0.7.1.tar.gz", hash = "sha256:f075d610b4648732fd17cbc9fa48be7d2c23df7b188fed3e4e6dde7bd1f14f20", size = 5124, upload-time = "2026-02-05T03:55:34.807Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/94/afa119b031c08e74a2e078260c5cdbb56b53949e5d2a0158215e8c265e12/nox_uv-0.7.0-py3-none-any.whl", hash = "sha256:51f9bb68ca6d721706f6372f0dec8ebfcc00408c2172b2bfeb6a6c06d9e3ab38", size = 5408, upload-time = "2026-01-03T19:54:57.798Z" }, + { url = "https://files.pythonhosted.org/packages/4f/0a/a6798a215366c9b034e92a9992d9013da5f544a488216fc54204ccf3c134/nox_uv-0.7.1-py3-none-any.whl", hash = "sha256:91361cc282a0a764de1b94ad002b67d5b43de4adc3f56e16d1b79928c8ec0433", size = 5457, upload-time = "2026-02-05T03:55:35.994Z" }, ] [[package]] @@ -680,27 +689,30 @@ wheels = [ [[package]] name = "pytest-asyncio" -version = "0.23.8" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/b4/0b378b7bf26a8ae161c3890c0b48a91a04106c5713ce81b4b080ea2f4f18/pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3", size = 46920, upload-time = "2024-07-17T17:39:34.617Z" } +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/82/62e2d63639ecb0fbe8a7ee59ef0bc69a4669ec50f6d3459f74ad4e4189a2/pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2", size = 17663, upload-time = "2024-07-17T17:39:32.478Z" }, + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] [[package]] name = "pytest-cov" -version = "5.0.0" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042, upload-time = "2024-03-24T20:16:34.856Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990, upload-time = "2024-03-24T20:16:32.444Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] @@ -729,15 +741,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/80/69756670caedcf3b9be597a6e12276a6cf6197076eb62aad0c608f8efce0/ruff-0.14.5-py3-none-win_arm64.whl", hash = "sha256:4b700459d4649e2594b31f20a9de33bc7c19976d4746d8d0798ad959621d64a4", size = 13433331, upload-time = "2025-11-13T19:58:48.434Z" }, ] -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - [[package]] name = "sniffio" version = "1.3.1" @@ -852,7 +855,6 @@ dev = [ { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "ruff" }, - { name = "six" }, ] lint = [ { name = "ruff" }, @@ -865,7 +867,6 @@ test = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, - { name = "six" }, ] type-check = [ { name = "pyright" }, @@ -881,24 +882,22 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "nox", specifier = ">=2024.10.9" }, - { name = "nox-uv", specifier = ">=0.7.0" }, - { name = "pyright", specifier = ">=1.1.408" }, - { name = "pytest", specifier = "==8.3.4" }, - { name = "pytest-asyncio", specifier = "==0.23.8" }, - { name = "pytest-cov", specifier = "==5.0.0" }, + { name = "nox", specifier = "~=2026.2" }, + { name = "nox-uv", specifier = "~=0.7" }, + { name = "pyright", specifier = "~=1.1" }, + { name = "pytest", specifier = "~=8.3" }, + { name = "pytest-asyncio", specifier = "~=1.3" }, + { name = "pytest-cov", specifier = "~=7.0" }, { name = "ruff", specifier = "==0.14.5" }, - { name = "six", specifier = "==1.17.0" }, ] lint = [{ name = "ruff", specifier = "==0.14.5" }] nox = [ - { name = "nox", specifier = ">=2024.10.9" }, - { name = "nox-uv", specifier = ">=0.7.0" }, + { name = "nox", specifier = "~=2026.2" }, + { name = "nox-uv", specifier = "~=0.7" }, ] test = [ - { name = "pytest", specifier = "==8.3.4" }, - { name = "pytest-asyncio", specifier = "==0.23.8" }, - { name = "pytest-cov", specifier = "==5.0.0" }, - { name = "six", specifier = "==1.17.0" }, + { name = "pytest", specifier = "~=8.3" }, + { name = "pytest-asyncio", specifier = "~=1.3" }, + { name = "pytest-cov", specifier = "~=7.0" }, ] -type-check = [{ name = "pyright", specifier = ">=1.1.408" }] +type-check = [{ name = "pyright", specifier = "~=1.1" }] From 33e6aecff69b2bdcaa1863dab9b1a18b946476da Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Mar 2026 20:04:20 -0400 Subject: [PATCH 04/26] fix: resolve 38 pre-existing pyright errors in tests and fixtures - Add pyrightconfig.json to configure pyright for this codebase - Fix test type errors with proper annotations and constructors - Fix dead assertions in test_audit_logs.py Co-Authored-By: Claude Opus 4.6 (1M context) --- pyrightconfig.json | 5 +++++ tests/conftest.py | 2 +- tests/test_audit_logs.py | 12 ++++++------ tests/test_directory_sync.py | 14 +++++++++----- tests/test_events.py | 1 + tests/test_sso.py | 1 + tests/test_user_management.py | 12 ++++++------ tests/test_vault.py | 13 ++++++++----- tests/utils/fixtures/mock_api_key.py | 5 +++-- tests/utils/fixtures/mock_organization_role.py | 4 +++- tests/utils/fixtures/mock_vault_object.py | 4 ++-- 11 files changed, 45 insertions(+), 28 deletions(-) create mode 100644 pyrightconfig.json diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 00000000..467902ec --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,5 @@ +{ + "include": ["src", "tests"], + "exclude": ["noxfile.py", ".nox", ".venv", "**/__pycache__"], + "reportIncompatibleVariableOverride": false +} diff --git a/tests/conftest.py b/tests/conftest.py index b7dfdb35..8b8ad8a3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -87,6 +87,7 @@ def pytest_generate_tests(metafunc: pytest.Metafunc): module_classes = marker.args ids = [] arg_values = [] + arg_names = ["module_instance"] for module_class in module_classes: if module_class is None: @@ -110,7 +111,6 @@ def pytest_generate_tests(metafunc: pytest.Metafunc): module_instance = module_class(**class_kwargs) ids.append(setup_name) # sync or async will be the test ID - arg_names = ["module_instance"] arg_values.append([module_instance]) metafunc.parametrize( diff --git a/tests/test_audit_logs.py b/tests/test_audit_logs.py index 1ff7929d..4bde30a9 100644 --- a/tests/test_audit_logs.py +++ b/tests/test_audit_logs.py @@ -162,12 +162,12 @@ def test_throws_badrequest_excpetion( organization_id=organization_id, event=mock_audit_log_event ) ) - assert excinfo.code == "invalid_audit_log" - assert excinfo.errors == ["error in a field"] - assert ( - excinfo.message - == "Audit Log could not be processed due to missing or incorrect data." - ) + assert excinfo.value.code == "invalid_audit_log" + assert excinfo.value.errors == ["error in a field"] + assert ( + excinfo.value.message + == "Audit Log could not be processed due to missing or incorrect data." + ) class TestCreateExport: def test_succeeds( diff --git a/tests/test_directory_sync.py b/tests/test_directory_sync.py index 61a80c98..b9be86ef 100644 --- a/tests/test_directory_sync.py +++ b/tests/test_directory_sync.py @@ -17,6 +17,10 @@ AsyncDirectorySync, DirectorySync, ) +from workos.types.directory_sync.list_filters import ( + DirectoryGroupListFilters, + DirectoryUserListFilters, +) def api_directory_to_sdk(directory): @@ -389,28 +393,28 @@ class TestPrepareRequestParams: """ def test_translates_directory_id_to_directory(self): - params = {"directory_id": "dir_123", "limit": 10} + params: DirectoryUserListFilters = {"directory_id": "dir_123", "limit": 10} result = _prepare_request_params(params) assert "directory" in result assert "directory_id" not in result assert result["directory"] == "dir_123" def test_translates_group_id_to_group(self): - params = {"group_id": "grp_123", "limit": 10} + params: DirectoryUserListFilters = {"group_id": "grp_123", "limit": 10} result = _prepare_request_params(params) assert "group" in result assert "group_id" not in result assert result["group"] == "grp_123" def test_translates_user_id_to_user(self): - params = {"user_id": "usr_123", "limit": 10} + params: DirectoryGroupListFilters = {"user_id": "usr_123", "limit": 10} result = _prepare_request_params(params) assert "user" in result assert "user_id" not in result assert result["user"] == "usr_123" def test_preserves_non_id_params(self): - params = { + params: DirectoryUserListFilters = { "directory_id": "dir_123", "limit": 10, "order": "desc", @@ -422,6 +426,6 @@ def test_preserves_non_id_params(self): assert result["after"] == "cursor" def test_handles_empty_params(self): - params = {"limit": 10, "order": "desc"} + params: DirectoryUserListFilters = {"limit": 10, "order": "desc"} result = _prepare_request_params(params) assert result == {"limit": 10, "order": "desc"} diff --git a/tests/test_events.py b/tests/test_events.py index c6122578..799fc34c 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -189,6 +189,7 @@ def test_list_events_vault_data_created( assert event.data.actor_name == "Test User" assert event.data.kv_name == "my-secret" assert event.data.key_id == "key_01234" + assert event.data.key_context is not None assert event.data.key_context.root == {"env": "production"} def test_list_events_vault_dek_read( diff --git a/tests/test_sso.py b/tests/test_sso.py index 4abc585d..6de4450f 100644 --- a/tests/test_sso.py +++ b/tests/test_sso.py @@ -375,6 +375,7 @@ def test_update_connection( } assert updated_connection.id == "conn_01FHT48Z8J8295GZNQ4ZP1J81T" assert updated_connection.name == "Foo Corporation" + assert updated_connection.options is not None assert updated_connection.options.signing_cert == "signing_cert" def test_delete_connection(self, capture_and_mock_http_client_request): diff --git a/tests/test_user_management.py b/tests/test_user_management.py index 19069abd..13763690 100644 --- a/tests/test_user_management.py +++ b/tests/test_user_management.py @@ -1,5 +1,5 @@ import json -from typing import Union +from typing import Any, Union from urllib.parse import parse_qsl, urlparse import pytest @@ -510,7 +510,7 @@ def test_update_user_with_locale( self.http_client, mock_user, 200 ) - params = { + params: dict[str, Any] = { "first_name": "Marcelina", "locale": "fr-FR", } @@ -700,7 +700,7 @@ def test_authenticate_with_code( mock_auth_response, base_authentication_params, ): - params = { + params: dict[str, Any] = { "code": "test_code", "code_verifier": "test_code_verifier", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", @@ -730,7 +730,7 @@ def test_authenticate_impersonator_with_code( mock_auth_response_with_impersonator, base_authentication_params, ): - params = {"code": "test_code"} + params: dict[str, Any] = {"code": "test_code"} request_kwargs = capture_and_mock_http_client_request( self.http_client, mock_auth_response_with_impersonator, 200 @@ -758,7 +758,7 @@ def test_authenticate_with_code_with_invitation_token( mock_auth_response, base_authentication_params, ): - params = { + params: dict[str, Any] = { "code": "test_code", "code_verifier": "test_code_verifier", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", @@ -916,7 +916,7 @@ def test_authenticate_with_refresh_token( mock_auth_refresh_token_response, base_authentication_params, ): - params = { + params: dict[str, Any] = { "refresh_token": "refresh_token_98765", "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", "ip_address": "192.0.0.1", diff --git a/tests/test_vault.py b/tests/test_vault.py index ed03fa61..0af5fea4 100644 --- a/tests/test_vault.py +++ b/tests/test_vault.py @@ -1,3 +1,5 @@ +from typing import Any, cast + import pytest from tests.utils.fixtures.mock_vault_object import ( @@ -105,7 +107,7 @@ def test_read_object_none_object_id(self): with pytest.raises( ValueError, match="Incomplete arguments: 'object_id' is a required argument" ): - self.vault.read_object(object_id=None) + self.vault.read_object(object_id=cast(str, None)) def test_read_object_by_name_success( self, mock_vault_object, capture_and_mock_http_client_request @@ -133,7 +135,7 @@ def test_read_object_by_name_none_name(self): with pytest.raises( ValueError, match="Incomplete arguments: 'name' is a required argument" ): - self.vault.read_object_by_name(name=None) + self.vault.read_object_by_name(name=cast(str, None)) def test_list_objects_default_params( self, mock_vault_objects_list, capture_and_mock_http_client_request @@ -297,7 +299,8 @@ def test_update_object_missing_value(self): with pytest.raises( TypeError, match="missing 1 required keyword-only argument: 'value'" ): - self.vault.update_object(object_id="vault_01234567890abcdef") + kwargs: dict[str, Any] = {"object_id": "vault_01234567890abcdef"} + self.vault.update_object(**kwargs) def test_update_object_missing_object_id(self): with pytest.raises( @@ -310,7 +313,7 @@ def test_update_object_none_object_id(self): ValueError, match="Incomplete arguments: 'object_id' is a required argument", ): - self.vault.update_object(object_id=None, value="updated-value") + self.vault.update_object(object_id=cast(str, None), value="updated-value") def test_delete_object_success(self, capture_and_mock_http_client_request): request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 204) @@ -331,7 +334,7 @@ def test_delete_object_none_object_id(self): with pytest.raises( ValueError, match="Incomplete arguments: 'object_id' is a required argument" ): - self.vault.delete_object(object_id=None) + self.vault.delete_object(object_id=cast(str, None)) def test_create_data_key_success( self, mock_data_key_pair, capture_and_mock_http_client_request diff --git a/tests/utils/fixtures/mock_api_key.py b/tests/utils/fixtures/mock_api_key.py index 27b2862a..0bd2d7cb 100644 --- a/tests/utils/fixtures/mock_api_key.py +++ b/tests/utils/fixtures/mock_api_key.py @@ -1,6 +1,7 @@ import datetime from workos.types.api_keys import ApiKey, ApiKeyWithValue +from workos.types.api_keys.api_keys import ApiKeyOwner class MockApiKey(ApiKey): @@ -9,7 +10,7 @@ def __init__(self, id="api_key_01234567890"): super().__init__( object="api_key", id=id, - owner={"type": "organization", "id": "org_1337"}, + owner=ApiKeyOwner(type="organization", id="org_1337"), name="Development API Key", obfuscated_value="api_..0", permissions=[], @@ -25,7 +26,7 @@ def __init__(self, id="api_key_01234567890"): super().__init__( object="api_key", id=id, - owner={"type": "organization", "id": "org_1337"}, + owner=ApiKeyOwner(type="organization", id="org_1337"), name="Development API Key", obfuscated_value="sk_...xyz", value="sk_live_abc123xyz", diff --git a/tests/utils/fixtures/mock_organization_role.py b/tests/utils/fixtures/mock_organization_role.py index e66607e9..61f7a9b6 100644 --- a/tests/utils/fixtures/mock_organization_role.py +++ b/tests/utils/fixtures/mock_organization_role.py @@ -1,4 +1,5 @@ import datetime +from typing import Any from workos.types.authorization.organization_role import OrganizationRole @@ -10,10 +11,10 @@ def __init__( organization_id: str = "org_01EHT88Z8J8795GZNQ4ZP1J81T", ): now = datetime.datetime.now().isoformat() + extra: dict[str, Any] = {"organization_id": organization_id} super().__init__( object="role", id=id, - organization_id=organization_id, name="Admin", slug="admin", description="Organization admin role", @@ -22,4 +23,5 @@ def __init__( type="OrganizationRole", created_at=now, updated_at=now, + **extra, ) diff --git a/tests/utils/fixtures/mock_vault_object.py b/tests/utils/fixtures/mock_vault_object.py index 007c59b6..1a7b0f23 100644 --- a/tests/utils/fixtures/mock_vault_object.py +++ b/tests/utils/fixtures/mock_vault_object.py @@ -20,7 +20,7 @@ def __init__( name=name, value=value, metadata=ObjectMetadata( - context=KeyContext(key="test-key"), + context=KeyContext({"key": "test-key"}), environment_id="env_01234567890abcdef", id=id, key_id="key_01234567890abcdef", @@ -43,7 +43,7 @@ class MockObjectMetadata(ObjectMetadata): def __init__(self, id="vault_01234567890abcdef"): now = datetime.datetime.now().isoformat() super().__init__( - context=KeyContext(key="test-key"), + context=KeyContext({"key": "test-key"}), environment_id="env_01234567890abcdef", id=id, key_id="key_01234567890abcdef", From f2196c142a748a6a114b679251baf907e15a76c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 27 Mar 2026 20:23:55 -0400 Subject: [PATCH 05/26] fix: set asyncio_default_fixture_loop_scope to silence deprecation warning Future pytest-asyncio versions will default async fixture loop scope to "function". Set it explicitly to avoid surprises on upgrade. Co-Authored-By: Claude Opus 4.6 (1M context) --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 0b99b418..e581128e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,9 @@ type_check = ["pyright~=1.1"] nox = ["nox~=2026.2", "nox-uv~=0.7"] +[tool.pytest.ini_options] +asyncio_default_fixture_loop_scope = "function" + [tool.ruff.lint.per-file-ignores] "*/__init__.py" = ["F401", "F403"] From 6e0b60f52f4f33f0b9e7efacc04991a92ca60c27 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Mon, 30 Mar 2026 11:58:50 -0400 Subject: [PATCH 06/26] add `pytest-httpx` to `test` group --- pyproject.toml | 2 +- uv.lock | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e581128e..553d6731 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dev = [ { include-group = "nox" }, "pyright>=1.1.408", ] -test = ["pytest~=8.3", "pytest-asyncio~=1.3", "pytest-cov~=7.0"] +test = ["pytest~=8.3", "pytest-asyncio~=1.3", "pytest-cov~=7.0", "pytest-httpx~=0.35.0"] lint = ["ruff==0.14.5"] type_check = ["pyright~=1.1"] nox = ["nox~=2026.2", "nox-uv~=0.7"] diff --git a/uv.lock b/uv.lock index 5fab0e13..07209838 100644 --- a/uv.lock +++ b/uv.lock @@ -715,6 +715,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] +[[package]] +name = "pytest-httpx" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/89/5b12b7b29e3d0af3a4b9c071ee92fa25a9017453731a38f08ba01c280f4c/pytest_httpx-0.35.0.tar.gz", hash = "sha256:d619ad5d2e67734abfbb224c3d9025d64795d4b8711116b1a13f72a251ae511f", size = 54146, upload-time = "2024-11-28T19:16:54.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/ed/026d467c1853dd83102411a78126b4842618e86c895f93528b0528c7a620/pytest_httpx-0.35.0-py3-none-any.whl", hash = "sha256:ee11a00ffcea94a5cbff47af2114d34c5b231c326902458deed73f9c459fd744", size = 19442, upload-time = "2024-11-28T19:16:52.787Z" }, +] + [[package]] name = "ruff" version = "0.14.5" @@ -854,6 +867,7 @@ dev = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, + { name = "pytest-httpx" }, { name = "ruff" }, ] lint = [ @@ -867,6 +881,7 @@ test = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, + { name = "pytest-httpx" }, ] type-check = [ { name = "pyright" }, @@ -888,6 +903,7 @@ dev = [ { name = "pytest", specifier = "~=8.3" }, { name = "pytest-asyncio", specifier = "~=1.3" }, { name = "pytest-cov", specifier = "~=7.0" }, + { name = "pytest-httpx", specifier = "~=0.35.0" }, { name = "ruff", specifier = "==0.14.5" }, ] lint = [{ name = "ruff", specifier = "==0.14.5" }] @@ -899,5 +915,6 @@ test = [ { name = "pytest", specifier = "~=8.3" }, { name = "pytest-asyncio", specifier = "~=1.3" }, { name = "pytest-cov", specifier = "~=7.0" }, + { name = "pytest-httpx", specifier = "~=0.35.0" }, ] type-check = [{ name = "pyright", specifier = "~=1.1" }] From 7184dd247586a16bcaebbc021798d5329abe84cb Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Mon, 30 Mar 2026 15:43:30 -0400 Subject: [PATCH 07/26] update dependencies --- pyproject.toml | 21 ++++-- uv.lock | 197 ++++++++++++++++++++++++++----------------------- 2 files changed, 116 insertions(+), 102 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 553d6731..5d48e242 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,14 +4,14 @@ version = "5.46.0" description = "WorkOS Python Client" readme = "README.md" license = "MIT" -authors = [{ name = "WorkOS", email = "team@workos.com" }] +authors = [{ name = "WorkOS", email = "sdk@workos.com" }] requires-python = ">=3.10" dependencies = [ - "cryptography>=44.0.2", - "httpx~=0.28.1", - "pydantic>=2.10.4", - "pyjwt>=2.12.0", + "cryptography~=46.0", + "httpx~=0.28", + "pydantic~=2.12", + "pyjwt~=2.12", ] [project.urls] @@ -27,8 +27,13 @@ dev = [ { include-group = "nox" }, "pyright>=1.1.408", ] -test = ["pytest~=8.3", "pytest-asyncio~=1.3", "pytest-cov~=7.0", "pytest-httpx~=0.35.0"] -lint = ["ruff==0.14.5"] +test = [ + "pytest~=9.0", + "pytest-asyncio~=1.3", + "pytest-cov~=7.1", + "pytest-httpx~=0.36", + ] +lint = ["ruff~=0.15"] type_check = ["pyright~=1.1"] nox = ["nox~=2026.2", "nox-uv~=0.7"] @@ -51,5 +56,5 @@ typeCheckingMode = "strict" include = ["workos"] [build-system] -requires = ["uv_build>=0.8.15,<0.9.0"] +requires = ["uv_build~=0.11"] build-backend = "uv_build" diff --git a/uv.lock b/uv.lock index 07209838..966ad92b 100644 --- a/uv.lock +++ b/uv.lock @@ -271,62 +271,62 @@ toml = [ [[package]] name = "cryptography" -version = "46.0.5" +version = "46.0.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, - { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, - { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, - { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, - { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, - { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, - { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, - { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, - { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, - { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, - { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, - { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, - { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, - { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, - { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, - { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, - { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" }, + { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" }, + { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" }, + { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" }, + { url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147, upload-time = "2026-03-25T23:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" }, + { url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" }, + { url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" }, + { url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" }, + { url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" }, + { url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511, upload-time = "2026-03-25T23:34:09.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692, upload-time = "2026-03-25T23:34:11.613Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" }, + { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" }, + { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" }, + { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" }, + { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" }, + { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" }, + { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" }, + { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" }, + { url = "https://files.pythonhosted.org/packages/2e/84/7ccff00ced5bac74b775ce0beb7d1be4e8637536b522b5df9b73ada42da2/cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead", size = 3475444, upload-time = "2026-03-25T23:34:38.944Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1f/4c926f50df7749f000f20eede0c896769509895e2648db5da0ed55db711d/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8", size = 4218227, upload-time = "2026-03-25T23:34:40.871Z" }, + { url = "https://files.pythonhosted.org/packages/c6/65/707be3ffbd5f786028665c3223e86e11c4cda86023adbc56bd72b1b6bab5/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0", size = 4381399, upload-time = "2026-03-25T23:34:42.609Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6d/73557ed0ef7d73d04d9aba745d2c8e95218213687ee5e76b7d236a5030fc/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b", size = 4217595, upload-time = "2026-03-25T23:34:44.205Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c5/e1594c4eec66a567c3ac4400008108a415808be2ce13dcb9a9045c92f1a0/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a", size = 4380912, upload-time = "2026-03-25T23:34:46.328Z" }, + { url = "https://files.pythonhosted.org/packages/1a/89/843b53614b47f97fe1abc13f9a86efa5ec9e275292c457af1d4a60dc80e0/cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e", size = 3409955, upload-time = "2026-03-25T23:34:48.465Z" }, ] [[package]] @@ -514,7 +514,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.4" +version = "2.12.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -522,9 +522,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" }, + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [[package]] @@ -645,6 +645,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, ] +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + [[package]] name = "pyjwt" version = "2.12.1" @@ -672,7 +681,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.3.4" +version = "9.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -680,11 +689,12 @@ dependencies = [ { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, + { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919, upload-time = "2024-12-01T12:54:25.98Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083, upload-time = "2024-12-01T12:54:19.735Z" }, + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] @@ -717,41 +727,40 @@ wheels = [ [[package]] name = "pytest-httpx" -version = "0.35.0" +version = "0.36.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/89/5b12b7b29e3d0af3a4b9c071ee92fa25a9017453731a38f08ba01c280f4c/pytest_httpx-0.35.0.tar.gz", hash = "sha256:d619ad5d2e67734abfbb224c3d9025d64795d4b8711116b1a13f72a251ae511f", size = 54146, upload-time = "2024-11-28T19:16:54.237Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/5574834da9499066fa1a5ea9c336f94dba2eae02298d36dab192fcf95c86/pytest_httpx-0.36.0.tar.gz", hash = "sha256:9edb66a5fd4388ce3c343189bc67e7e1cb50b07c2e3fc83b97d511975e8a831b", size = 56793, upload-time = "2025-12-02T16:34:57.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/ed/026d467c1853dd83102411a78126b4842618e86c895f93528b0528c7a620/pytest_httpx-0.35.0-py3-none-any.whl", hash = "sha256:ee11a00ffcea94a5cbff47af2114d34c5b231c326902458deed73f9c459fd744", size = 19442, upload-time = "2024-11-28T19:16:52.787Z" }, + { url = "https://files.pythonhosted.org/packages/e2/d2/1eb1ea9c84f0d2033eb0b49675afdc71aa4ea801b74615f00f3c33b725e3/pytest_httpx-0.36.0-py3-none-any.whl", hash = "sha256:bd4c120bb80e142df856e825ec9f17981effb84d159f9fa29ed97e2357c3a9c8", size = 20229, upload-time = "2025-12-02T16:34:56.45Z" }, ] [[package]] name = "ruff" -version = "0.14.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/fa/fbb67a5780ae0f704876cb8ac92d6d76da41da4dc72b7ed3565ab18f2f52/ruff-0.14.5.tar.gz", hash = "sha256:8d3b48d7d8aad423d3137af7ab6c8b1e38e4de104800f0d596990f6ada1a9fc1", size = 5615944, upload-time = "2025-11-13T19:58:51.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/31/c07e9c535248d10836a94e4f4e8c5a31a1beed6f169b31405b227872d4f4/ruff-0.14.5-py3-none-linux_armv6l.whl", hash = "sha256:f3b8248123b586de44a8018bcc9fefe31d23dda57a34e6f0e1e53bd51fd63594", size = 13171630, upload-time = "2025-11-13T19:57:54.894Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5c/283c62516dca697cd604c2796d1487396b7a436b2f0ecc3fd412aca470e0/ruff-0.14.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f7a75236570318c7a30edd7f5491945f0169de738d945ca8784500b517163a72", size = 13413925, upload-time = "2025-11-13T19:57:59.181Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f3/aa319f4afc22cb6fcba2b9cdfc0f03bbf747e59ab7a8c5e90173857a1361/ruff-0.14.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6d146132d1ee115f8802356a2dc9a634dbf58184c51bff21f313e8cd1c74899a", size = 12574040, upload-time = "2025-11-13T19:58:02.056Z" }, - { url = "https://files.pythonhosted.org/packages/f9/7f/cb5845fcc7c7e88ed57f58670189fc2ff517fe2134c3821e77e29fd3b0c8/ruff-0.14.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2380596653dcd20b057794d55681571a257a42327da8894b93bbd6111aa801f", size = 13009755, upload-time = "2025-11-13T19:58:05.172Z" }, - { url = "https://files.pythonhosted.org/packages/21/d2/bcbedbb6bcb9253085981730687ddc0cc7b2e18e8dc13cf4453de905d7a0/ruff-0.14.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d1fa985a42b1f075a098fa1ab9d472b712bdb17ad87a8ec86e45e7fa6273e68", size = 12937641, upload-time = "2025-11-13T19:58:08.345Z" }, - { url = "https://files.pythonhosted.org/packages/a4/58/e25de28a572bdd60ffc6bb71fc7fd25a94ec6a076942e372437649cbb02a/ruff-0.14.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88f0770d42b7fa02bbefddde15d235ca3aa24e2f0137388cc15b2dcbb1f7c7a7", size = 13610854, upload-time = "2025-11-13T19:58:11.419Z" }, - { url = "https://files.pythonhosted.org/packages/7d/24/43bb3fd23ecee9861970978ea1a7a63e12a204d319248a7e8af539984280/ruff-0.14.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3676cb02b9061fee7294661071c4709fa21419ea9176087cb77e64410926eb78", size = 15061088, upload-time = "2025-11-13T19:58:14.551Z" }, - { url = "https://files.pythonhosted.org/packages/23/44/a022f288d61c2f8c8645b24c364b719aee293ffc7d633a2ca4d116b9c716/ruff-0.14.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b595bedf6bc9cab647c4a173a61acf4f1ac5f2b545203ba82f30fcb10b0318fb", size = 14734717, upload-time = "2025-11-13T19:58:17.518Z" }, - { url = "https://files.pythonhosted.org/packages/58/81/5c6ba44de7e44c91f68073e0658109d8373b0590940efe5bd7753a2585a3/ruff-0.14.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f55382725ad0bdb2e8ee2babcbbfb16f124f5a59496a2f6a46f1d9d99d93e6e2", size = 14028812, upload-time = "2025-11-13T19:58:20.533Z" }, - { url = "https://files.pythonhosted.org/packages/ad/ef/41a8b60f8462cb320f68615b00299ebb12660097c952c600c762078420f8/ruff-0.14.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7497d19dce23976bdaca24345ae131a1d38dcfe1b0850ad8e9e6e4fa321a6e19", size = 13825656, upload-time = "2025-11-13T19:58:23.345Z" }, - { url = "https://files.pythonhosted.org/packages/7c/00/207e5de737fdb59b39eb1fac806904fe05681981b46d6a6db9468501062e/ruff-0.14.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:410e781f1122d6be4f446981dd479470af86537fb0b8857f27a6e872f65a38e4", size = 13959922, upload-time = "2025-11-13T19:58:26.537Z" }, - { url = "https://files.pythonhosted.org/packages/bc/7e/fa1f5c2776db4be405040293618846a2dece5c70b050874c2d1f10f24776/ruff-0.14.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01be527ef4c91a6d55e53b337bfe2c0f82af024cc1a33c44792d6844e2331e1", size = 12932501, upload-time = "2025-11-13T19:58:29.822Z" }, - { url = "https://files.pythonhosted.org/packages/67/d8/d86bf784d693a764b59479a6bbdc9515ae42c340a5dc5ab1dabef847bfaa/ruff-0.14.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f66e9bb762e68d66e48550b59c74314168ebb46199886c5c5aa0b0fbcc81b151", size = 12927319, upload-time = "2025-11-13T19:58:32.923Z" }, - { url = "https://files.pythonhosted.org/packages/ac/de/ee0b304d450ae007ce0cb3e455fe24fbcaaedae4ebaad6c23831c6663651/ruff-0.14.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d93be8f1fa01022337f1f8f3bcaa7ffee2d0b03f00922c45c2207954f351f465", size = 13206209, upload-time = "2025-11-13T19:58:35.952Z" }, - { url = "https://files.pythonhosted.org/packages/33/aa/193ca7e3a92d74f17d9d5771a765965d2cf42c86e6f0fd95b13969115723/ruff-0.14.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:c135d4b681f7401fe0e7312017e41aba9b3160861105726b76cfa14bc25aa367", size = 13953709, upload-time = "2025-11-13T19:58:39.002Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f1/7119e42aa1d3bf036ffc9478885c2e248812b7de9abea4eae89163d2929d/ruff-0.14.5-py3-none-win32.whl", hash = "sha256:c83642e6fccfb6dea8b785eb9f456800dcd6a63f362238af5fc0c83d027dd08b", size = 12925808, upload-time = "2025-11-13T19:58:42.779Z" }, - { url = "https://files.pythonhosted.org/packages/3b/9d/7c0a255d21e0912114784e4a96bf62af0618e2190cae468cd82b13625ad2/ruff-0.14.5-py3-none-win_amd64.whl", hash = "sha256:9d55d7af7166f143c94eae1db3312f9ea8f95a4defef1979ed516dbb38c27621", size = 14331546, upload-time = "2025-11-13T19:58:45.691Z" }, - { url = "https://files.pythonhosted.org/packages/e5/80/69756670caedcf3b9be597a6e12276a6cf6197076eb62aad0c608f8efce0/ruff-0.14.5-py3-none-win_arm64.whl", hash = "sha256:4b700459d4649e2594b31f20a9de33bc7c19976d4746d8d0798ad959621d64a4", size = 13433331, upload-time = "2025-11-13T19:58:48.434Z" }, +version = "0.15.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" }, + { url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" }, + { url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" }, + { url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" }, + { url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" }, + { url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" }, + { url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" }, + { url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" }, + { url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" }, + { url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" }, + { url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, ] [[package]] @@ -889,10 +898,10 @@ type-check = [ [package.metadata] requires-dist = [ - { name = "cryptography", specifier = ">=44.0.2" }, - { name = "httpx", specifier = "~=0.28.1" }, - { name = "pydantic", specifier = ">=2.10.4" }, - { name = "pyjwt", specifier = ">=2.12.0" }, + { name = "cryptography", specifier = "~=46.0" }, + { name = "httpx", specifier = "~=0.28" }, + { name = "pydantic", specifier = "~=2.12" }, + { name = "pyjwt", specifier = "~=2.12" }, ] [package.metadata.requires-dev] @@ -900,21 +909,21 @@ dev = [ { name = "nox", specifier = "~=2026.2" }, { name = "nox-uv", specifier = "~=0.7" }, { name = "pyright", specifier = "~=1.1" }, - { name = "pytest", specifier = "~=8.3" }, + { name = "pytest", specifier = "~=9.0" }, { name = "pytest-asyncio", specifier = "~=1.3" }, - { name = "pytest-cov", specifier = "~=7.0" }, - { name = "pytest-httpx", specifier = "~=0.35.0" }, - { name = "ruff", specifier = "==0.14.5" }, + { name = "pytest-cov", specifier = "~=7.1" }, + { name = "pytest-httpx", specifier = "~=0.36" }, + { name = "ruff", specifier = "~=0.15" }, ] -lint = [{ name = "ruff", specifier = "==0.14.5" }] +lint = [{ name = "ruff", specifier = "~=0.15" }] nox = [ { name = "nox", specifier = "~=2026.2" }, { name = "nox-uv", specifier = "~=0.7" }, ] test = [ - { name = "pytest", specifier = "~=8.3" }, + { name = "pytest", specifier = "~=9.0" }, { name = "pytest-asyncio", specifier = "~=1.3" }, - { name = "pytest-cov", specifier = "~=7.0" }, - { name = "pytest-httpx", specifier = "~=0.35.0" }, + { name = "pytest-cov", specifier = "~=7.1" }, + { name = "pytest-httpx", specifier = "~=0.36" }, ] type-check = [{ name = "pyright", specifier = "~=1.1" }] From f57e7cca9b9d7f733429160af96841d7f64be862 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Mon, 30 Mar 2026 15:43:48 -0400 Subject: [PATCH 08/26] swap mypy in lint CI for pyright --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3bb245e7..a6b7563d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -31,4 +31,4 @@ jobs: run: uv run ruff check --extend-exclude .devbox - name: Type check - run: uv run mypy + run: uv run pyright From dab75bdb991a55aa68d3e3c50d8274a124a2cc66 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Mon, 30 Mar 2026 22:18:17 -0400 Subject: [PATCH 09/26] remove legacy types/, compat layer, and handwritten service modules Delete the entire legacy stack that the generated code replaces: - All 10 _compat.py bridge files - All handwritten service modules (sso.py, connect.py, etc.) - Old client.py, async_client.py, _base_client.py, _client_configuration.py - Entire types/ directory (~170 files of Pydantic models) - typing/ directory (legacy type helpers) - utils/ directory (legacy HTTP client, request helpers) - exceptions.py (replaced by _errors.py) - All legacy test files and fixtures The generated _resource.py + _client.py + models/ are now the sole implementation. Update __init__.py to export from the generated _client.py. Add new conftest.py with workos/async_workos fixtures for the generated test suite. 436 tests pass, 0 failures. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/workos/__init__.py | 47 +- src/workos/_base_client.py | 156 -- src/workos/_client_configuration.py | 12 - src/workos/api_keys.py | 77 - src/workos/async_client.py | 160 -- src/workos/audit_logs.py | 561 ---- src/workos/authorization.py | 1734 ------------ src/workos/client.py | 164 -- src/workos/connect.py | 427 --- src/workos/directory_sync.py | 452 --- src/workos/events.py | 111 - src/workos/exceptions.py | 83 - src/workos/fga.py | 654 ----- src/workos/mfa.py | 205 -- src/workos/organization_domains.py | 179 -- src/workos/organizations.py | 602 ---- src/workos/passwordless.py | 91 - src/workos/pipes.py | 93 - src/workos/portal.py | 65 - src/workos/session.py | 360 --- src/workos/sso.py | 405 --- src/workos/types/__init__.py | 4 - src/workos/types/api_keys/__init__.py | 2 - src/workos/types/api_keys/api_keys.py | 26 - src/workos/types/api_keys/list_filters.py | 5 - src/workos/types/audit_logs/__init__.py | 12 - .../types/audit_logs/audit_log_action.py | 28 - .../audit_logs/audit_log_configuration.py | 41 - .../types/audit_logs/audit_log_event.py | 16 - .../types/audit_logs/audit_log_event_actor.py | 12 - .../audit_logs/audit_log_event_context.py | 8 - .../audit_logs/audit_log_event_target.py | 12 - .../types/audit_logs/audit_log_export.py | 18 - .../types/audit_logs/audit_log_metadata.py | 4 - .../types/audit_logs/audit_log_retention.py | 13 - .../types/audit_logs/audit_log_schema.py | 49 - .../audit_logs/audit_log_schema_input.py | 78 - src/workos/types/audit_logs/list_filters.py | 13 - src/workos/types/authorization/__init__.py | 33 - .../authorization/access_check_response.py | 5 - src/workos/types/authorization/assignment.py | 3 - .../authorization/authorization_resource.py | 18 - .../types/authorization/environment_role.py | 21 - .../authorization/organization_membership.py | 5 - .../types/authorization/organization_role.py | 35 - .../parent_resource_identifier.py | 15 - src/workos/types/authorization/permission.py | 15 - .../authorization/resource_identifier.py | 15 - src/workos/types/authorization/role.py | 18 - .../types/authorization/role_assignment.py | 22 - src/workos/types/connect/__init__.py | 2 - src/workos/types/connect/client_secret.py | 13 - .../types/connect/connect_application.py | 29 - src/workos/types/connect/list_filters.py | 7 - .../types/connect/redirect_uri_input.py | 9 - src/workos/types/directory_sync/__init__.py | 5 - src/workos/types/directory_sync/directory.py | 31 - .../types/directory_sync/directory_group.py | 16 - .../types/directory_sync/directory_state.py | 28 - .../types/directory_sync/directory_type.py | 24 - .../types/directory_sync/directory_user.py | 50 - .../types/directory_sync/list_filters.py | 21 - src/workos/types/events/__init__.py | 16 - .../types/events/authentication_payload.py | 104 - .../connection_payload_with_legacy_fields.py | 5 - .../connection_saml_certificate_payload.py | 30 - .../directory_group_membership_payload.py | 9 - ...irectory_group_with_previous_attributes.py | 6 - src/workos/types/events/directory_payload.py | 16 - .../directory_payload_with_legacy_fields.py | 29 - ...directory_user_with_previous_attributes.py | 6 - src/workos/types/events/event.py | 504 ---- src/workos/types/events/event_model.py | 146 - src/workos/types/events/event_type.py | 88 - src/workos/types/events/flag_payload.py | 70 - src/workos/types/events/list_filters.py | 10 - ...tion_domain_verification_failed_payload.py | 14 - .../types/events/previous_attributes.py | 3 - src/workos/types/events/session_payload.py | 27 - src/workos/types/events/vault_payload.py | 65 - src/workos/types/feature_flags/__init__.py | 3 - .../types/feature_flags/feature_flag.py | 12 - .../types/feature_flags/list_filters.py | 5 - src/workos/types/fga/__init__.py | 5 - .../types/fga/authorization_resource_types.py | 9 - .../types/fga/authorization_resources.py | 10 - src/workos/types/fga/check.py | 51 - src/workos/types/fga/list_filters.py | 24 - src/workos/types/fga/warnings.py | 33 - src/workos/types/fga/warrant.py | 49 - src/workos/types/list_resource.py | 216 -- src/workos/types/metadata.py | 4 - src/workos/types/mfa/__init__.py | 5 - .../types/mfa/authentication_challenge.py | 14 - ...ication_challenge_verification_response.py | 9 - src/workos/types/mfa/authentication_factor.py | 69 - ...tion_factor_totp_and_challenge_response.py | 10 - .../mfa/enroll_authentication_factor_type.py | 8 - .../types/organization_domains/__init__.py | 1 - .../organization_domain.py | 18 - src/workos/types/organizations/__init__.py | 6 - .../types/organizations/domain_data_input.py | 7 - .../types/organizations/list_filters.py | 6 - .../types/organizations/organization.py | 12 - .../organizations/organization_common.py | 13 - src/workos/types/passwordless/__init__.py | 2 - .../passwordless/passwordless_session.py | 12 - .../passwordless/passwordless_session_type.py | 3 - src/workos/types/pipes/__init__.py | 6 - src/workos/types/pipes/pipes.py | 34 - src/workos/types/portal/__init__.py | 2 - src/workos/types/portal/portal_link.py | 7 - src/workos/types/portal/portal_link_intent.py | 11 - .../portal/portal_link_intent_options.py | 9 - src/workos/types/roles/__init__.py | 0 src/workos/types/roles/role.py | 29 - src/workos/types/sso/__init__.py | 4 - src/workos/types/sso/connection.py | 70 - src/workos/types/sso/connection_domain.py | 8 - src/workos/types/sso/profile.py | 35 - src/workos/types/sso/sso_provider_type.py | 10 - src/workos/types/user_management/__init__.py | 12 - .../authenticate_with_common.py | 66 - .../authentication_response.py | 53 - .../user_management/email_verification.py | 18 - .../types/user_management/impersonator.py | 8 - .../types/user_management/invitation.py | 27 - .../types/user_management/list_filters.py | 29 - .../types/user_management/magic_auth.py | 18 - .../types/user_management/oauth_tokens.py | 21 - .../organization_membership.py | 32 - .../user_management/password_hash_type.py | 5 - .../types/user_management/password_reset.py | 18 - .../types/user_management/screen_hint.py | 3 - src/workos/types/user_management/session.py | 78 - src/workos/types/user_management/user.py | 22 - .../user_management_provider_type.py | 11 - src/workos/types/vault/__init__.py | 2 - src/workos/types/vault/key.py | 25 - src/workos/types/vault/object.py | 38 - src/workos/types/webhooks/__init__.py | 0 src/workos/types/webhooks/webhook.py | 456 ---- src/workos/types/webhooks/webhook_model.py | 14 - src/workos/types/webhooks/webhook_payload.py | 4 - src/workos/types/widgets/__init__.py | 2 - src/workos/types/widgets/widget_scope.py | 4 - .../types/widgets/widget_token_response.py | 7 - src/workos/types/workos_model.py | 26 - src/workos/typing/__init__.py | 1 - src/workos/typing/literals.py | 32 - src/workos/typing/sync_or_async.py | 5 - src/workos/typing/untyped_literal.py | 37 - src/workos/typing/webhooks.py | 18 - src/workos/user_management.py | 2428 ----------------- src/workos/utils/__init__.py | 0 src/workos/utils/_base_http_client.py | 255 -- src/workos/utils/crypto_provider.py | 39 - src/workos/utils/http_client.py | 262 -- src/workos/utils/pagination_order.py | 4 - src/workos/utils/request_helper.py | 27 - src/workos/vault.py | 543 ---- src/workos/webhooks.py | 142 - src/workos/widgets.py | 54 - tests/conftest.py | 355 +-- tests/smoke_test.py | 272 -- tests/test_api_keys.py | 70 - tests/test_async_http_client.py | 368 --- tests/test_audit_logs.py | 883 ------ tests/test_authorization.py | 494 ---- tests/test_authorization_check.py | 142 - tests/test_authorization_resource.py | 767 ------ ...test_authorization_resource_external_id.py | 297 -- ...test_authorization_resource_memberships.py | 904 ------ tests/test_authorization_role_assignments.py | 358 --- tests/test_client.py | 128 - tests/test_connect.py | 261 -- tests/test_directory_sync.py | 431 --- tests/test_events.py | 514 ---- tests/test_fga.py | 652 ----- tests/test_mfa.py | 257 -- tests/test_organization_domains.py | 136 - tests/test_organizations.py | 414 --- tests/test_passwordless.py | 57 - tests/test_pipes.py | 167 -- tests/test_portal.py | 117 - tests/test_session.py | 794 ------ tests/test_sso.py | 403 --- tests/test_sync_http_client.py | 407 --- tests/test_user_management.py | 1324 --------- tests/test_user_management_list_sessions.py | 73 - tests/test_user_management_revoke_session.py | 26 - tests/test_vault.py | 489 ---- tests/test_webhooks.py | 156 -- tests/test_widgets.py | 25 - tests/types/test_auto_pagination_function.py | 4 - tests/types/test_directory_state.py | 33 - tests/utils/__init__.py | 0 tests/utils/client_configuration.py | 33 - tests/utils/fixtures/__init__.py | 0 tests/utils/fixtures/mock_api_key.py | 37 - tests/utils/fixtures/mock_auth_factor_totp.py | 23 - tests/utils/fixtures/mock_client_secret.py | 19 - .../fixtures/mock_connect_application.py | 29 - tests/utils/fixtures/mock_connection.py | 29 - tests/utils/fixtures/mock_directory.py | 35 - tests/utils/fixtures/mock_directory_group.py | 19 - tests/utils/fixtures/mock_directory_user.py | 53 - .../utils/fixtures/mock_email_verification.py | 18 - tests/utils/fixtures/mock_environment_role.py | 20 - tests/utils/fixtures/mock_event.py | 29 - tests/utils/fixtures/mock_feature_flag.py | 17 - tests/utils/fixtures/mock_invitation.py | 23 - tests/utils/fixtures/mock_magic_auth.py | 18 - tests/utils/fixtures/mock_organization.py | 28 - .../fixtures/mock_organization_membership.py | 67 - .../utils/fixtures/mock_organization_role.py | 27 - tests/utils/fixtures/mock_password_reset.py | 18 - tests/utils/fixtures/mock_permission.py | 19 - tests/utils/fixtures/mock_profile.py | 29 - tests/utils/fixtures/mock_resource.py | 30 - tests/utils/fixtures/mock_resource_list.py | 45 - tests/utils/fixtures/mock_role.py | 18 - tests/utils/fixtures/mock_role_assignment.py | 68 - tests/utils/fixtures/mock_user.py | 22 - tests/utils/fixtures/mock_vault_object.py | 63 - tests/utils/list_resource.py | 15 - tests/utils/syncify.py | 20 - tests/utils/test_request_helper.py | 12 - 228 files changed, 55 insertions(+), 26860 deletions(-) delete mode 100644 src/workos/_base_client.py delete mode 100644 src/workos/_client_configuration.py delete mode 100644 src/workos/api_keys.py delete mode 100644 src/workos/async_client.py delete mode 100644 src/workos/audit_logs.py delete mode 100644 src/workos/authorization.py delete mode 100644 src/workos/client.py delete mode 100644 src/workos/connect.py delete mode 100644 src/workos/directory_sync.py delete mode 100644 src/workos/events.py delete mode 100644 src/workos/exceptions.py delete mode 100644 src/workos/fga.py delete mode 100644 src/workos/mfa.py delete mode 100644 src/workos/organization_domains.py delete mode 100644 src/workos/organizations.py delete mode 100644 src/workos/passwordless.py delete mode 100644 src/workos/pipes.py delete mode 100644 src/workos/portal.py delete mode 100644 src/workos/session.py delete mode 100644 src/workos/sso.py delete mode 100644 src/workos/types/__init__.py delete mode 100644 src/workos/types/api_keys/__init__.py delete mode 100644 src/workos/types/api_keys/api_keys.py delete mode 100644 src/workos/types/api_keys/list_filters.py delete mode 100644 src/workos/types/audit_logs/__init__.py delete mode 100644 src/workos/types/audit_logs/audit_log_action.py delete mode 100644 src/workos/types/audit_logs/audit_log_configuration.py delete mode 100644 src/workos/types/audit_logs/audit_log_event.py delete mode 100644 src/workos/types/audit_logs/audit_log_event_actor.py delete mode 100644 src/workos/types/audit_logs/audit_log_event_context.py delete mode 100644 src/workos/types/audit_logs/audit_log_event_target.py delete mode 100644 src/workos/types/audit_logs/audit_log_export.py delete mode 100644 src/workos/types/audit_logs/audit_log_metadata.py delete mode 100644 src/workos/types/audit_logs/audit_log_retention.py delete mode 100644 src/workos/types/audit_logs/audit_log_schema.py delete mode 100644 src/workos/types/audit_logs/audit_log_schema_input.py delete mode 100644 src/workos/types/audit_logs/list_filters.py delete mode 100644 src/workos/types/authorization/__init__.py delete mode 100644 src/workos/types/authorization/access_check_response.py delete mode 100644 src/workos/types/authorization/assignment.py delete mode 100644 src/workos/types/authorization/authorization_resource.py delete mode 100644 src/workos/types/authorization/environment_role.py delete mode 100644 src/workos/types/authorization/organization_membership.py delete mode 100644 src/workos/types/authorization/organization_role.py delete mode 100644 src/workos/types/authorization/parent_resource_identifier.py delete mode 100644 src/workos/types/authorization/permission.py delete mode 100644 src/workos/types/authorization/resource_identifier.py delete mode 100644 src/workos/types/authorization/role.py delete mode 100644 src/workos/types/authorization/role_assignment.py delete mode 100644 src/workos/types/connect/__init__.py delete mode 100644 src/workos/types/connect/client_secret.py delete mode 100644 src/workos/types/connect/connect_application.py delete mode 100644 src/workos/types/connect/list_filters.py delete mode 100644 src/workos/types/connect/redirect_uri_input.py delete mode 100644 src/workos/types/directory_sync/__init__.py delete mode 100644 src/workos/types/directory_sync/directory.py delete mode 100644 src/workos/types/directory_sync/directory_group.py delete mode 100644 src/workos/types/directory_sync/directory_state.py delete mode 100644 src/workos/types/directory_sync/directory_type.py delete mode 100644 src/workos/types/directory_sync/directory_user.py delete mode 100644 src/workos/types/directory_sync/list_filters.py delete mode 100644 src/workos/types/events/__init__.py delete mode 100644 src/workos/types/events/authentication_payload.py delete mode 100644 src/workos/types/events/connection_payload_with_legacy_fields.py delete mode 100644 src/workos/types/events/connection_saml_certificate_payload.py delete mode 100644 src/workos/types/events/directory_group_membership_payload.py delete mode 100644 src/workos/types/events/directory_group_with_previous_attributes.py delete mode 100644 src/workos/types/events/directory_payload.py delete mode 100644 src/workos/types/events/directory_payload_with_legacy_fields.py delete mode 100644 src/workos/types/events/directory_user_with_previous_attributes.py delete mode 100644 src/workos/types/events/event.py delete mode 100644 src/workos/types/events/event_model.py delete mode 100644 src/workos/types/events/event_type.py delete mode 100644 src/workos/types/events/flag_payload.py delete mode 100644 src/workos/types/events/list_filters.py delete mode 100644 src/workos/types/events/organization_domain_verification_failed_payload.py delete mode 100644 src/workos/types/events/previous_attributes.py delete mode 100644 src/workos/types/events/session_payload.py delete mode 100644 src/workos/types/events/vault_payload.py delete mode 100644 src/workos/types/feature_flags/__init__.py delete mode 100644 src/workos/types/feature_flags/feature_flag.py delete mode 100644 src/workos/types/feature_flags/list_filters.py delete mode 100644 src/workos/types/fga/__init__.py delete mode 100644 src/workos/types/fga/authorization_resource_types.py delete mode 100644 src/workos/types/fga/authorization_resources.py delete mode 100644 src/workos/types/fga/check.py delete mode 100644 src/workos/types/fga/list_filters.py delete mode 100644 src/workos/types/fga/warnings.py delete mode 100644 src/workos/types/fga/warrant.py delete mode 100644 src/workos/types/list_resource.py delete mode 100644 src/workos/types/metadata.py delete mode 100644 src/workos/types/mfa/__init__.py delete mode 100644 src/workos/types/mfa/authentication_challenge.py delete mode 100644 src/workos/types/mfa/authentication_challenge_verification_response.py delete mode 100644 src/workos/types/mfa/authentication_factor.py delete mode 100644 src/workos/types/mfa/authentication_factor_totp_and_challenge_response.py delete mode 100644 src/workos/types/mfa/enroll_authentication_factor_type.py delete mode 100644 src/workos/types/organization_domains/__init__.py delete mode 100644 src/workos/types/organization_domains/organization_domain.py delete mode 100644 src/workos/types/organizations/__init__.py delete mode 100644 src/workos/types/organizations/domain_data_input.py delete mode 100644 src/workos/types/organizations/list_filters.py delete mode 100644 src/workos/types/organizations/organization.py delete mode 100644 src/workos/types/organizations/organization_common.py delete mode 100644 src/workos/types/passwordless/__init__.py delete mode 100644 src/workos/types/passwordless/passwordless_session.py delete mode 100644 src/workos/types/passwordless/passwordless_session_type.py delete mode 100644 src/workos/types/pipes/__init__.py delete mode 100644 src/workos/types/pipes/pipes.py delete mode 100644 src/workos/types/portal/__init__.py delete mode 100644 src/workos/types/portal/portal_link.py delete mode 100644 src/workos/types/portal/portal_link_intent.py delete mode 100644 src/workos/types/portal/portal_link_intent_options.py delete mode 100644 src/workos/types/roles/__init__.py delete mode 100644 src/workos/types/roles/role.py delete mode 100644 src/workos/types/sso/__init__.py delete mode 100644 src/workos/types/sso/connection.py delete mode 100644 src/workos/types/sso/connection_domain.py delete mode 100644 src/workos/types/sso/profile.py delete mode 100644 src/workos/types/sso/sso_provider_type.py delete mode 100644 src/workos/types/user_management/__init__.py delete mode 100644 src/workos/types/user_management/authenticate_with_common.py delete mode 100644 src/workos/types/user_management/authentication_response.py delete mode 100644 src/workos/types/user_management/email_verification.py delete mode 100644 src/workos/types/user_management/impersonator.py delete mode 100644 src/workos/types/user_management/invitation.py delete mode 100644 src/workos/types/user_management/list_filters.py delete mode 100644 src/workos/types/user_management/magic_auth.py delete mode 100644 src/workos/types/user_management/oauth_tokens.py delete mode 100644 src/workos/types/user_management/organization_membership.py delete mode 100644 src/workos/types/user_management/password_hash_type.py delete mode 100644 src/workos/types/user_management/password_reset.py delete mode 100644 src/workos/types/user_management/screen_hint.py delete mode 100644 src/workos/types/user_management/session.py delete mode 100644 src/workos/types/user_management/user.py delete mode 100644 src/workos/types/user_management/user_management_provider_type.py delete mode 100644 src/workos/types/vault/__init__.py delete mode 100644 src/workos/types/vault/key.py delete mode 100644 src/workos/types/vault/object.py delete mode 100644 src/workos/types/webhooks/__init__.py delete mode 100644 src/workos/types/webhooks/webhook.py delete mode 100644 src/workos/types/webhooks/webhook_model.py delete mode 100644 src/workos/types/webhooks/webhook_payload.py delete mode 100644 src/workos/types/widgets/__init__.py delete mode 100644 src/workos/types/widgets/widget_scope.py delete mode 100644 src/workos/types/widgets/widget_token_response.py delete mode 100644 src/workos/types/workos_model.py delete mode 100644 src/workos/typing/__init__.py delete mode 100644 src/workos/typing/literals.py delete mode 100644 src/workos/typing/sync_or_async.py delete mode 100644 src/workos/typing/untyped_literal.py delete mode 100644 src/workos/typing/webhooks.py delete mode 100644 src/workos/user_management.py delete mode 100644 src/workos/utils/__init__.py delete mode 100644 src/workos/utils/_base_http_client.py delete mode 100644 src/workos/utils/crypto_provider.py delete mode 100644 src/workos/utils/http_client.py delete mode 100644 src/workos/utils/pagination_order.py delete mode 100644 src/workos/utils/request_helper.py delete mode 100644 src/workos/vault.py delete mode 100644 src/workos/webhooks.py delete mode 100644 src/workos/widgets.py delete mode 100644 tests/smoke_test.py delete mode 100644 tests/test_api_keys.py delete mode 100644 tests/test_async_http_client.py delete mode 100644 tests/test_audit_logs.py delete mode 100644 tests/test_authorization.py delete mode 100644 tests/test_authorization_check.py delete mode 100644 tests/test_authorization_resource.py delete mode 100644 tests/test_authorization_resource_external_id.py delete mode 100644 tests/test_authorization_resource_memberships.py delete mode 100644 tests/test_authorization_role_assignments.py delete mode 100644 tests/test_client.py delete mode 100644 tests/test_connect.py delete mode 100644 tests/test_directory_sync.py delete mode 100644 tests/test_events.py delete mode 100644 tests/test_fga.py delete mode 100644 tests/test_mfa.py delete mode 100644 tests/test_organization_domains.py delete mode 100644 tests/test_organizations.py delete mode 100644 tests/test_passwordless.py delete mode 100644 tests/test_pipes.py delete mode 100644 tests/test_portal.py delete mode 100644 tests/test_session.py delete mode 100644 tests/test_sso.py delete mode 100644 tests/test_sync_http_client.py delete mode 100644 tests/test_user_management.py delete mode 100644 tests/test_user_management_list_sessions.py delete mode 100644 tests/test_user_management_revoke_session.py delete mode 100644 tests/test_vault.py delete mode 100644 tests/test_webhooks.py delete mode 100644 tests/test_widgets.py delete mode 100644 tests/types/test_auto_pagination_function.py delete mode 100644 tests/types/test_directory_state.py delete mode 100644 tests/utils/__init__.py delete mode 100644 tests/utils/client_configuration.py delete mode 100644 tests/utils/fixtures/__init__.py delete mode 100644 tests/utils/fixtures/mock_api_key.py delete mode 100644 tests/utils/fixtures/mock_auth_factor_totp.py delete mode 100644 tests/utils/fixtures/mock_client_secret.py delete mode 100644 tests/utils/fixtures/mock_connect_application.py delete mode 100644 tests/utils/fixtures/mock_connection.py delete mode 100644 tests/utils/fixtures/mock_directory.py delete mode 100644 tests/utils/fixtures/mock_directory_group.py delete mode 100644 tests/utils/fixtures/mock_directory_user.py delete mode 100644 tests/utils/fixtures/mock_email_verification.py delete mode 100644 tests/utils/fixtures/mock_environment_role.py delete mode 100644 tests/utils/fixtures/mock_event.py delete mode 100644 tests/utils/fixtures/mock_feature_flag.py delete mode 100644 tests/utils/fixtures/mock_invitation.py delete mode 100644 tests/utils/fixtures/mock_magic_auth.py delete mode 100644 tests/utils/fixtures/mock_organization.py delete mode 100644 tests/utils/fixtures/mock_organization_membership.py delete mode 100644 tests/utils/fixtures/mock_organization_role.py delete mode 100644 tests/utils/fixtures/mock_password_reset.py delete mode 100644 tests/utils/fixtures/mock_permission.py delete mode 100644 tests/utils/fixtures/mock_profile.py delete mode 100644 tests/utils/fixtures/mock_resource.py delete mode 100644 tests/utils/fixtures/mock_resource_list.py delete mode 100644 tests/utils/fixtures/mock_role.py delete mode 100644 tests/utils/fixtures/mock_role_assignment.py delete mode 100644 tests/utils/fixtures/mock_user.py delete mode 100644 tests/utils/fixtures/mock_vault_object.py delete mode 100644 tests/utils/list_resource.py delete mode 100644 tests/utils/syncify.py delete mode 100644 tests/utils/test_request_helper.py diff --git a/src/workos/__init__.py b/src/workos/__init__.py index fe4fd7ec..ab4133bb 100644 --- a/src/workos/__init__.py +++ b/src/workos/__init__.py @@ -1,4 +1,45 @@ -from workos.client import SyncClient as WorkOSClient -from workos.async_client import AsyncClient as AsyncWorkOSClient +"""WorkOS Python SDK.""" -__all__ = ["WorkOSClient", "AsyncWorkOSClient"] +from ._client import AsyncWorkOS, WorkOS +from ._errors import ( + WorkOSError, + AuthenticationError, + BadRequestError, + ConflictError, + ConfigurationError, + ForbiddenError, + NotFoundError, + RateLimitExceededError, + ServerError, + UnprocessableEntityError, + WorkOSConnectionError, + WorkOSTimeoutError, +) +from ._pagination import AsyncPage, SyncPage +from ._types import RequestOptions + +# Backward-compatible aliases +WorkOSClient = WorkOS +AsyncWorkOSClient = AsyncWorkOS + +__all__ = [ + "AsyncWorkOS", + "AsyncWorkOSClient", + "WorkOS", + "WorkOSClient", + "RequestOptions", + "WorkOSError", + "AuthenticationError", + "BadRequestError", + "ConflictError", + "ConfigurationError", + "ForbiddenError", + "NotFoundError", + "RateLimitExceededError", + "ServerError", + "UnprocessableEntityError", + "WorkOSConnectionError", + "WorkOSTimeoutError", + "AsyncPage", + "SyncPage", +] diff --git a/src/workos/_base_client.py b/src/workos/_base_client.py deleted file mode 100644 index d883aec2..00000000 --- a/src/workos/_base_client.py +++ /dev/null @@ -1,156 +0,0 @@ -import os -from abc import abstractmethod -from typing import Optional - -from workos._client_configuration import ClientConfiguration -from workos.api_keys import ApiKeysModule -from workos.audit_logs import AuditLogsModule -from workos.authorization import AuthorizationModule -from workos.connect import ConnectModule -from workos.directory_sync import DirectorySyncModule -from workos.events import EventsModule -from workos.fga import FGAModule -from workos.mfa import MFAModule -from workos.organization_domains import OrganizationDomainsModule -from workos.pipes import PipesModule -from workos.organizations import OrganizationsModule -from workos.passwordless import PasswordlessModule -from workos.portal import PortalModule -from workos.sso import SSOModule -from workos.user_management import UserManagementModule -from workos.utils._base_http_client import DEFAULT_REQUEST_TIMEOUT -from workos.webhooks import WebhooksModule - - -class BaseClient(ClientConfiguration): - """Base client for accessing the WorkOS feature set.""" - - _api_key: str - _base_url: str - _client_id: str - _request_timeout: int - _jwt_leeway: float - - def __init__( - self, - *, - api_key: Optional[str], - client_id: Optional[str], - base_url: Optional[str] = None, - request_timeout: Optional[int] = None, - jwt_leeway: float = 0, - ) -> None: - api_key = api_key or os.getenv("WORKOS_API_KEY") - if api_key is None: - raise ValueError( - "WorkOS API key must be provided when instantiating the client or via the WORKOS_API_KEY environment variable." - ) - - self._api_key = api_key - - client_id = client_id or os.getenv("WORKOS_CLIENT_ID") - if client_id is None: - raise ValueError( - "WorkOS client ID must be provided when instantiating the client or via the WORKOS_CLIENT_ID environment variable." - ) - - self._client_id = client_id - - self._base_url = self._enforce_trailing_slash( - url=( - base_url - if base_url - else os.getenv("WORKOS_BASE_URL", "https://api.workos.com/") - ) - ) - - self._request_timeout = ( - request_timeout - if request_timeout - else int(os.getenv("WORKOS_REQUEST_TIMEOUT", DEFAULT_REQUEST_TIMEOUT)) - ) - - self._jwt_leeway = jwt_leeway - - @property - @abstractmethod - def api_keys(self) -> ApiKeysModule: ... - - @property - @abstractmethod - def authorization(self) -> AuthorizationModule: ... - - @property - @abstractmethod - def connect(self) -> ConnectModule: ... - - @property - @abstractmethod - def audit_logs(self) -> AuditLogsModule: ... - - @property - @abstractmethod - def directory_sync(self) -> DirectorySyncModule: ... - - @property - @abstractmethod - def events(self) -> EventsModule: ... - - @property - @abstractmethod - def fga(self) -> FGAModule: ... - - @property - @abstractmethod - def mfa(self) -> MFAModule: ... - - @property - @abstractmethod - def organizations(self) -> OrganizationsModule: ... - - @property - @abstractmethod - def organization_domains(self) -> OrganizationDomainsModule: ... - - @property - @abstractmethod - def passwordless(self) -> PasswordlessModule: ... - - @property - @abstractmethod - def pipes(self) -> PipesModule: ... - - @property - @abstractmethod - def portal(self) -> PortalModule: ... - - @property - @abstractmethod - def sso(self) -> SSOModule: ... - - @property - @abstractmethod - def user_management(self) -> UserManagementModule: ... - - @property - @abstractmethod - def webhooks(self) -> WebhooksModule: ... - - def _enforce_trailing_slash(self, url: str) -> str: - return url if url.endswith("/") else url + "/" - - @property - def base_url(self) -> str: - return self._base_url - - @property - def client_id(self) -> str: - return self._client_id - - @property - def request_timeout(self) -> int: - return self._request_timeout - - @property - def jwt_leeway(self) -> float: - return self._jwt_leeway diff --git a/src/workos/_client_configuration.py b/src/workos/_client_configuration.py deleted file mode 100644 index 6319bf71..00000000 --- a/src/workos/_client_configuration.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Protocol - - -class ClientConfiguration(Protocol): - @property - def base_url(self) -> str: ... - @property - def client_id(self) -> str: ... - @property - def request_timeout(self) -> int: ... - @property - def jwt_leeway(self) -> float: ... diff --git a/src/workos/api_keys.py b/src/workos/api_keys.py deleted file mode 100644 index 81b5931f..00000000 --- a/src/workos/api_keys.py +++ /dev/null @@ -1,77 +0,0 @@ -from typing import Optional, Protocol - -from workos.types.api_keys import ApiKey -from workos.typing.sync_or_async import SyncOrAsync -from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient -from workos.utils.request_helper import REQUEST_METHOD_DELETE, REQUEST_METHOD_POST - -API_KEYS_PATH = "api_keys" -API_KEY_VALIDATION_PATH = "api_keys/validations" -RESOURCE_OBJECT_ATTRIBUTE_NAME = "api_key" - - -class ApiKeysModule(Protocol): - def validate_api_key(self, *, value: str) -> SyncOrAsync[Optional[ApiKey]]: - """Validate an API key. - - Kwargs: - value (str): API key value - - Returns: - Optional[ApiKey]: Returns ApiKey resource object - if supplied value was valid, None if it was not - """ - ... - - def delete_api_key(self, api_key_id: str) -> SyncOrAsync[None]: - """Delete an API key. - - Args: - api_key_id (str): The ID of the API key to delete - - Returns: - None - """ - ... - - -class ApiKeys(ApiKeysModule): - _http_client: SyncHTTPClient - - def __init__(self, http_client: SyncHTTPClient): - self._http_client = http_client - - def validate_api_key(self, *, value: str) -> Optional[ApiKey]: - response = self._http_client.request( - API_KEY_VALIDATION_PATH, method=REQUEST_METHOD_POST, json={"value": value} - ) - if response.get(RESOURCE_OBJECT_ATTRIBUTE_NAME) is None: - return None - return ApiKey.model_validate(response[RESOURCE_OBJECT_ATTRIBUTE_NAME]) - - def delete_api_key(self, api_key_id: str) -> None: - self._http_client.request( - f"{API_KEYS_PATH}/{api_key_id}", - method=REQUEST_METHOD_DELETE, - ) - - -class AsyncApiKeys(ApiKeysModule): - _http_client: AsyncHTTPClient - - def __init__(self, http_client: AsyncHTTPClient): - self._http_client = http_client - - async def validate_api_key(self, *, value: str) -> Optional[ApiKey]: - response = await self._http_client.request( - API_KEY_VALIDATION_PATH, method=REQUEST_METHOD_POST, json={"value": value} - ) - if response.get(RESOURCE_OBJECT_ATTRIBUTE_NAME) is None: - return None - return ApiKey.model_validate(response[RESOURCE_OBJECT_ATTRIBUTE_NAME]) - - async def delete_api_key(self, api_key_id: str) -> None: - await self._http_client.request( - f"{API_KEYS_PATH}/{api_key_id}", - method=REQUEST_METHOD_DELETE, - ) diff --git a/src/workos/async_client.py b/src/workos/async_client.py deleted file mode 100644 index d2b3921c..00000000 --- a/src/workos/async_client.py +++ /dev/null @@ -1,160 +0,0 @@ -from typing import Optional -from importlib.metadata import version -from workos._base_client import BaseClient -from workos.api_keys import AsyncApiKeys -from workos.audit_logs import AsyncAuditLogs -from workos.authorization import AsyncAuthorization -from workos.connect import AsyncConnect -from workos.directory_sync import AsyncDirectorySync -from workos.events import AsyncEvents -from workos.fga import FGAModule -from workos.mfa import MFAModule -from workos.organizations import AsyncOrganizations -from workos.organization_domains import AsyncOrganizationDomains -from workos.passwordless import PasswordlessModule -from workos.pipes import AsyncPipes -from workos.portal import PortalModule -from workos.sso import AsyncSSO -from workos.user_management import AsyncUserManagement -from workos.utils.http_client import AsyncHTTPClient -from workos.webhooks import WebhooksModule -from workos.widgets import WidgetsModule -from workos.vault import VaultModule - - -class AsyncClient(BaseClient): - """Client for a convenient way to access the WorkOS feature set.""" - - _http_client: AsyncHTTPClient - - def __init__( - self, - *, - api_key: Optional[str] = None, - client_id: Optional[str] = None, - base_url: Optional[str] = None, - request_timeout: Optional[int] = None, - jwt_leeway: float = 0, - ): - super().__init__( - api_key=api_key, - client_id=client_id, - base_url=base_url, - request_timeout=request_timeout, - jwt_leeway=jwt_leeway, - ) - self._http_client = AsyncHTTPClient( - api_key=self._api_key, - base_url=self.base_url, - client_id=self._client_id, - version=version("workos"), - timeout=self.request_timeout, - ) - - @property - def connect(self) -> AsyncConnect: - if not getattr(self, "_connect", None): - self._connect = AsyncConnect(self._http_client) - return self._connect - - @property - def api_keys(self) -> AsyncApiKeys: - if not getattr(self, "_api_keys", None): - self._api_keys = AsyncApiKeys(self._http_client) - return self._api_keys - - @property - def authorization(self) -> AsyncAuthorization: - if not getattr(self, "_authorization", None): - self._authorization = AsyncAuthorization(self._http_client) - return self._authorization - - @property - def sso(self) -> AsyncSSO: - if not getattr(self, "_sso", None): - self._sso = AsyncSSO( - http_client=self._http_client, client_configuration=self - ) - return self._sso - - @property - def audit_logs(self) -> AsyncAuditLogs: - if not getattr(self, "_audit_logs", None): - self._audit_logs = AsyncAuditLogs(self._http_client) - return self._audit_logs - - @property - def directory_sync(self) -> AsyncDirectorySync: - if not getattr(self, "_directory_sync", None): - self._directory_sync = AsyncDirectorySync(self._http_client) - return self._directory_sync - - @property - def events(self) -> AsyncEvents: - if not getattr(self, "_events", None): - self._events = AsyncEvents(self._http_client) - return self._events - - @property - def fga(self) -> FGAModule: - raise NotImplementedError("FGA APIs are not yet supported in the async client.") - - @property - def organizations(self) -> AsyncOrganizations: - if not getattr(self, "_organizations", None): - self._organizations = AsyncOrganizations(self._http_client) - return self._organizations - - @property - def organization_domains(self) -> AsyncOrganizationDomains: - if not getattr(self, "_organization_domains", None): - self._organization_domains = AsyncOrganizationDomains( - http_client=self._http_client, client_configuration=self - ) - return self._organization_domains - - @property - def passwordless(self) -> PasswordlessModule: - raise NotImplementedError( - "Passwordless APIs are not yet supported in the async client." - ) - - @property - def pipes(self) -> AsyncPipes: - if not getattr(self, "_pipes", None): - self._pipes = AsyncPipes(self._http_client) - return self._pipes - - @property - def portal(self) -> PortalModule: - raise NotImplementedError( - "Portal APIs are not yet supported in the async client." - ) - - @property - def webhooks(self) -> WebhooksModule: - raise NotImplementedError("Webhooks are not yet supported in the async client.") - - @property - def mfa(self) -> MFAModule: - raise NotImplementedError("MFA APIs are not yet supported in the async client.") - - @property - def user_management(self) -> AsyncUserManagement: - if not getattr(self, "_user_management", None): - self._user_management = AsyncUserManagement( - http_client=self._http_client, client_configuration=self - ) - return self._user_management - - @property - def widgets(self) -> WidgetsModule: - raise NotImplementedError( - "Widgets APIs are not yet supported in the async client." - ) - - @property - def vault(self) -> VaultModule: - raise NotImplementedError( - "Vault APIs are not yet supported in the async client." - ) diff --git a/src/workos/audit_logs.py b/src/workos/audit_logs.py deleted file mode 100644 index 1b5a865f..00000000 --- a/src/workos/audit_logs.py +++ /dev/null @@ -1,561 +0,0 @@ -from typing import Dict, Literal, Optional, Protocol, Sequence - -from workos.types.audit_logs import ( - AuditLogAction, - AuditLogConfiguration, - AuditLogExport, - AuditLogRetention, - AuditLogSchema, - AuditLogSchemaListFilters, - AuditLogActionListFilters, -) -from workos.types.audit_logs.audit_log_schema_input import ( - AuditLogSchemaActorInput, - AuditLogSchemaTargetInput, - MetadataSchemaInput, - serialize_schema_options, -) -from workos.types.audit_logs.audit_log_event import AuditLogEvent -from workos.types.list_resource import ListMetadata, ListPage, WorkOSListResource -from workos.typing.sync_or_async import SyncOrAsync -from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient -from workos.utils.pagination_order import PaginationOrder -from workos.utils.request_helper import ( - DEFAULT_LIST_RESPONSE_LIMIT, - REQUEST_METHOD_GET, - REQUEST_METHOD_POST, - REQUEST_METHOD_PUT, -) - -EVENTS_PATH = "audit_logs/events" -EXPORTS_PATH = "audit_logs/exports" -ACTIONS_PATH = "audit_logs/actions" - - -AuditLogActionsListResource = WorkOSListResource[ - AuditLogAction, AuditLogActionListFilters, ListMetadata -] - -AuditLogSchemasListResource = WorkOSListResource[ - AuditLogSchema, AuditLogSchemaListFilters, ListMetadata -] - - -class AuditLogsModule(Protocol): - """Offers methods through the WorkOS Audit Logs service.""" - - def create_event( - self, - *, - organization_id: str, - event: AuditLogEvent, - idempotency_key: Optional[str] = None, - ) -> SyncOrAsync[None]: - """Create an Audit Logs event. - - Kwargs: - organization_id (str): Organization's unique identifier. - event (AuditLogEvent): An AuditLogEvent object. - idempotency_key (str): Idempotency key. (Optional) - Returns: - None - """ - ... - - def create_export( - self, - *, - organization_id: str, - range_start: str, - range_end: str, - actions: Optional[Sequence[str]] = None, - targets: Optional[Sequence[str]] = None, - actor_names: Optional[Sequence[str]] = None, - actor_ids: Optional[Sequence[str]] = None, - ) -> SyncOrAsync[AuditLogExport]: - """Trigger the creation of an export of audit logs. - - Kwargs: - organization_id (str): Organization's unique identifier. - range_start (str): Start date of the date range filter. - range_end (str): End date of the date range filter. - actions (list): Optional list of actions to filter. (Optional) - actor_names (list): Optional list of actors to filter by name. (Optional) - actor_ids (list): Optional list of actors to filter by ID. (Optional) - targets (list): Optional list of targets to filter. (Optional) - - Returns: - AuditLogExport: Object that describes the audit log export - """ - ... - - def get_export(self, audit_log_export_id: str) -> SyncOrAsync[AuditLogExport]: - """Retrieve a created export. - - Args: - audit_log_export_id (str): Audit log export unique identifier. - - Returns: - AuditLogExport: Object that describes the audit log export - """ - ... - - def create_schema( - self, - *, - action: str, - targets: Sequence[AuditLogSchemaTargetInput], - actor: Optional[AuditLogSchemaActorInput] = None, - metadata: Optional[MetadataSchemaInput] = None, - idempotency_key: Optional[str] = None, - ) -> SyncOrAsync[AuditLogSchema]: - """Create an Audit Log schema for an action. - - Kwargs: - action (str): The action name for the schema (e.g., 'user.signed_in'). - targets (list): List of target definitions with type and optional metadata. - Each target has a 'type' and optional 'metadata' mapping property - names to types (e.g., {"status": "string"}). - actor (dict): Optional actor definition with metadata schema. (Optional) - The metadata maps property names to types (e.g., {"role": "string"}). - metadata (dict): Optional event-level metadata schema. (Optional) - Maps property names to types (e.g., {"invoice_id": "string"}). - idempotency_key (str): Idempotency key. (Optional) - - Returns: - AuditLogSchema: The created audit log schema - """ - ... - - def list_schemas( - self, - *, - action: str, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[AuditLogSchemasListResource]: - """List all schemas for an Audit Log action. - - Kwargs: - action (str): The action name to list schemas for. - limit (int): Maximum number of records to return. (Optional) - before (str): Pagination cursor to receive records before a provided ID. (Optional) - after (str): Pagination cursor to receive records after a provided ID. (Optional) - order (Literal["asc","desc"]): Sort order by created_at timestamp. (Optional) - - Returns: - AuditLogSchemasListResource: Paginated list of audit log schemas - """ - ... - - def list_actions( - self, - *, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[AuditLogActionsListResource]: - """List all registered Audit Log actions. - - Kwargs: - limit (int): Maximum number of records to return. (Optional) - before (str): Pagination cursor to receive records before a provided ID. (Optional) - after (str): Pagination cursor to receive records after a provided ID. (Optional) - order (Literal["asc","desc"]): Sort order by created_at timestamp. (Optional) - - Returns: - AuditLogActionsListResource: Paginated list of audit log actions - """ - ... - - def get_retention(self, organization_id: str) -> SyncOrAsync[AuditLogRetention]: - """Get the event retention period for an organization. - - Args: - organization_id (str): Organization's unique identifier. - - Returns: - AuditLogRetention: The retention configuration - """ - ... - - def set_retention( - self, - *, - organization_id: str, - retention_period_in_days: Literal[30, 365], - ) -> SyncOrAsync[AuditLogRetention]: - """Set the event retention period for an organization. - - Kwargs: - organization_id (str): Organization's unique identifier. - retention_period_in_days (int): The number of days to retain events (30 or 365). - - Returns: - AuditLogRetention: The updated retention configuration - """ - ... - - def get_configuration( - self, organization_id: str - ) -> SyncOrAsync[AuditLogConfiguration]: - """Get the audit log configuration for an organization. - - Args: - organization_id (str): Organization's unique identifier. - - Returns: - AuditLogConfiguration: The complete audit log configuration - """ - ... - - -class AuditLogs(AuditLogsModule): - _http_client: SyncHTTPClient - - def __init__(self, http_client: SyncHTTPClient): - self._http_client = http_client - - def create_event( - self, - *, - organization_id: str, - event: AuditLogEvent, - idempotency_key: Optional[str] = None, - ) -> None: - json = {"organization_id": organization_id, "event": event} - - headers: Dict[str, str] = {} - if idempotency_key: - headers["idempotency-key"] = idempotency_key - - self._http_client.request( - EVENTS_PATH, method=REQUEST_METHOD_POST, json=json, headers=headers - ) - - def create_export( - self, - *, - organization_id: str, - range_start: str, - range_end: str, - actions: Optional[Sequence[str]] = None, - targets: Optional[Sequence[str]] = None, - actor_names: Optional[Sequence[str]] = None, - actor_ids: Optional[Sequence[str]] = None, - ) -> AuditLogExport: - json = { - "actions": actions, - "actor_ids": actor_ids, - "actor_names": actor_names, - "organization_id": organization_id, - "range_start": range_start, - "range_end": range_end, - "targets": targets, - } - - response = self._http_client.request( - EXPORTS_PATH, method=REQUEST_METHOD_POST, json=json - ) - - return AuditLogExport.model_validate(response) - - def get_export(self, audit_log_export_id: str) -> AuditLogExport: - response = self._http_client.request( - f"{EXPORTS_PATH}/{audit_log_export_id}", - method=REQUEST_METHOD_GET, - ) - - return AuditLogExport.model_validate(response) - - def create_schema( - self, - *, - action: str, - targets: Sequence[AuditLogSchemaTargetInput], - actor: Optional[AuditLogSchemaActorInput] = None, - metadata: Optional[MetadataSchemaInput] = None, - idempotency_key: Optional[str] = None, - ) -> AuditLogSchema: - json = serialize_schema_options(targets, actor, metadata) - - headers: Dict[str, str] = {} - if idempotency_key: - headers["idempotency-key"] = idempotency_key - - response = self._http_client.request( - f"{ACTIONS_PATH}/{action}/schemas", - method=REQUEST_METHOD_POST, - json=json, - headers=headers, - ) - - return AuditLogSchema.model_validate(response) - - def list_schemas( - self, - *, - action: str, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> AuditLogSchemasListResource: - list_params: AuditLogSchemaListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = self._http_client.request( - f"{ACTIONS_PATH}/{action}/schemas", - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return WorkOSListResource[ - AuditLogSchema, AuditLogSchemaListFilters, ListMetadata - ]( - list_method=lambda **kwargs: self.list_schemas(action=action, **kwargs), - list_args=list_params, - **ListPage[AuditLogSchema](**response).model_dump(), - ) - - def list_actions( - self, - *, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> AuditLogActionsListResource: - list_params: AuditLogActionListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = self._http_client.request( - ACTIONS_PATH, - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return WorkOSListResource[ - AuditLogAction, AuditLogActionListFilters, ListMetadata - ]( - list_method=self.list_actions, - list_args=list_params, - **ListPage[AuditLogAction](**response).model_dump(), - ) - - def get_retention(self, organization_id: str) -> AuditLogRetention: - response = self._http_client.request( - f"organizations/{organization_id}/audit_logs_retention", - method=REQUEST_METHOD_GET, - ) - - return AuditLogRetention.model_validate(response) - - def set_retention( - self, - *, - organization_id: str, - retention_period_in_days: Literal[30, 365], - ) -> AuditLogRetention: - json = {"retention_period_in_days": retention_period_in_days} - - response = self._http_client.request( - f"organizations/{organization_id}/audit_logs_retention", - method=REQUEST_METHOD_PUT, - json=json, - ) - - return AuditLogRetention.model_validate(response) - - def get_configuration(self, organization_id: str) -> AuditLogConfiguration: - response = self._http_client.request( - f"organizations/{organization_id}/audit_log_configuration", - method=REQUEST_METHOD_GET, - ) - - return AuditLogConfiguration.model_validate(response) - - -class AsyncAuditLogs(AuditLogsModule): - _http_client: AsyncHTTPClient - - def __init__(self, http_client: AsyncHTTPClient): - self._http_client = http_client - - async def create_event( - self, - *, - organization_id: str, - event: AuditLogEvent, - idempotency_key: Optional[str] = None, - ) -> None: - json = {"organization_id": organization_id, "event": event} - - headers: Dict[str, str] = {} - if idempotency_key: - headers["idempotency-key"] = idempotency_key - - await self._http_client.request( - EVENTS_PATH, method=REQUEST_METHOD_POST, json=json, headers=headers - ) - - async def create_export( - self, - *, - organization_id: str, - range_start: str, - range_end: str, - actions: Optional[Sequence[str]] = None, - targets: Optional[Sequence[str]] = None, - actor_names: Optional[Sequence[str]] = None, - actor_ids: Optional[Sequence[str]] = None, - ) -> AuditLogExport: - json = { - "actions": actions, - "actor_ids": actor_ids, - "actor_names": actor_names, - "organization_id": organization_id, - "range_start": range_start, - "range_end": range_end, - "targets": targets, - } - - response = await self._http_client.request( - EXPORTS_PATH, method=REQUEST_METHOD_POST, json=json - ) - - return AuditLogExport.model_validate(response) - - async def get_export(self, audit_log_export_id: str) -> AuditLogExport: - response = await self._http_client.request( - f"{EXPORTS_PATH}/{audit_log_export_id}", - method=REQUEST_METHOD_GET, - ) - - return AuditLogExport.model_validate(response) - - async def create_schema( - self, - *, - action: str, - targets: Sequence[AuditLogSchemaTargetInput], - actor: Optional[AuditLogSchemaActorInput] = None, - metadata: Optional[MetadataSchemaInput] = None, - idempotency_key: Optional[str] = None, - ) -> AuditLogSchema: - json = serialize_schema_options(targets, actor, metadata) - - headers: Dict[str, str] = {} - if idempotency_key: - headers["idempotency-key"] = idempotency_key - - response = await self._http_client.request( - f"{ACTIONS_PATH}/{action}/schemas", - method=REQUEST_METHOD_POST, - json=json, - headers=headers, - ) - - return AuditLogSchema.model_validate(response) - - async def list_schemas( - self, - *, - action: str, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> AuditLogSchemasListResource: - list_params: AuditLogSchemaListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = await self._http_client.request( - f"{ACTIONS_PATH}/{action}/schemas", - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return WorkOSListResource[ - AuditLogSchema, AuditLogSchemaListFilters, ListMetadata - ]( - list_method=lambda **kwargs: self.list_schemas(action=action, **kwargs), - list_args=list_params, - **ListPage[AuditLogSchema](**response).model_dump(), - ) - - async def list_actions( - self, - *, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> AuditLogActionsListResource: - list_params: AuditLogActionListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = await self._http_client.request( - ACTIONS_PATH, - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return WorkOSListResource[ - AuditLogAction, AuditLogActionListFilters, ListMetadata - ]( - list_method=self.list_actions, - list_args=list_params, - **ListPage[AuditLogAction](**response).model_dump(), - ) - - async def get_retention(self, organization_id: str) -> AuditLogRetention: - response = await self._http_client.request( - f"organizations/{organization_id}/audit_logs_retention", - method=REQUEST_METHOD_GET, - ) - - return AuditLogRetention.model_validate(response) - - async def set_retention( - self, - *, - organization_id: str, - retention_period_in_days: Literal[30, 365], - ) -> AuditLogRetention: - json = {"retention_period_in_days": retention_period_in_days} - - response = await self._http_client.request( - f"organizations/{organization_id}/audit_logs_retention", - method=REQUEST_METHOD_PUT, - json=json, - ) - - return AuditLogRetention.model_validate(response) - - async def get_configuration(self, organization_id: str) -> AuditLogConfiguration: - response = await self._http_client.request( - f"organizations/{organization_id}/audit_log_configuration", - method=REQUEST_METHOD_GET, - ) - - return AuditLogConfiguration.model_validate(response) diff --git a/src/workos/authorization.py b/src/workos/authorization.py deleted file mode 100644 index 24364f11..00000000 --- a/src/workos/authorization.py +++ /dev/null @@ -1,1734 +0,0 @@ -from enum import Enum -from functools import partial -from typing import Any, Dict, Optional, Protocol, Sequence, Union - -from pydantic import TypeAdapter - -from workos.types.authorization.access_check_response import AccessCheckResponse -from workos.types.authorization.assignment import Assignment -from workos.types.authorization.environment_role import ( - EnvironmentRole, - EnvironmentRoleList, -) -from workos.types.authorization.organization_membership import ( - AuthorizationOrganizationMembership, -) -from workos.types.authorization.organization_role import OrganizationRole -from workos.types.authorization.parent_resource_identifier import ( - ParentResourceIdentifier, -) -from workos.types.authorization.permission import Permission -from workos.types.authorization.resource_identifier import ResourceIdentifier -from workos.types.authorization.authorization_resource import AuthorizationResource -from workos.types.authorization.role import Role, RoleList -from workos.types.authorization.role_assignment import RoleAssignment -from workos.types.list_resource import ( - ListArgs, - ListMetadata, - ListPage, - WorkOSListResource, -) -from workos.typing.sync_or_async import SyncOrAsync -from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient -from workos.utils.pagination_order import PaginationOrder -from workos.utils.request_helper import ( - DEFAULT_LIST_RESPONSE_LIMIT, - REQUEST_METHOD_DELETE, - REQUEST_METHOD_GET, - REQUEST_METHOD_PATCH, - REQUEST_METHOD_POST, - REQUEST_METHOD_PUT, -) - - -class _Unset(Enum): - TOKEN = 0 - - -UNSET: _Unset = _Unset.TOKEN - -AUTHORIZATION_PERMISSIONS_PATH = "authorization/permissions" -AUTHORIZATION_RESOURCES_PATH = "authorization/resources" -AUTHORIZATION_ORGANIZATIONS_PATH = "authorization/organizations" -AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH = "authorization/organization_memberships" - - -class ResourceListFilters(ListArgs, total=False): - organization_id: Optional[str] - resource_type_slug: Optional[str] - parent_resource_id: Optional[str] - parent_resource_type_slug: Optional[str] - parent_external_id: Optional[str] - search: Optional[str] - - -AuthorizationResourcesList = WorkOSListResource[ - AuthorizationResource, ResourceListFilters, ListMetadata -] - - -class ResourcesForMembershipListFilters(ListArgs, total=False): - permission_slug: str - - -AuthorizationResourcesForMembershipList = WorkOSListResource[ - AuthorizationResource, ResourcesForMembershipListFilters, ListMetadata -] - - -class AuthorizationOrganizationMembershipListFilters(ListArgs, total=False): - permission_slug: str - assignment: Optional[Assignment] - - -AuthorizationOrganizationMembershipList = WorkOSListResource[ - AuthorizationOrganizationMembership, - AuthorizationOrganizationMembershipListFilters, - ListMetadata, -] - -_role_adapter: TypeAdapter[Role] = TypeAdapter(Role) - - -class RoleAssignmentListFilters(ListArgs, total=False): - organization_membership_id: str - - -RoleAssignmentsListResource = WorkOSListResource[ - RoleAssignment, RoleAssignmentListFilters, ListMetadata -] - - -class PermissionListFilters(ListArgs, total=False): - pass - - -PermissionsListResource = WorkOSListResource[ - Permission, PermissionListFilters, ListMetadata -] - - -class AuthorizationModule(Protocol): - """Offers methods through the WorkOS Authorization service.""" - - def create_permission( - self, - *, - slug: str, - name: str, - description: Optional[str] = None, - resource_type_slug: Optional[str] = None, - ) -> SyncOrAsync[Permission]: ... - - def list_permissions( - self, - *, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[PermissionsListResource]: ... - - def get_permission(self, slug: str) -> SyncOrAsync[Permission]: ... - - def update_permission( - self, - slug: str, - *, - name: Optional[str] = None, - description: Optional[str] = None, - ) -> SyncOrAsync[Permission]: ... - - def delete_permission(self, slug: str) -> SyncOrAsync[None]: ... - - # Organization Roles - - def create_organization_role( - self, - organization_id: str, - *, - slug: str, - name: str, - description: Optional[str] = None, - ) -> SyncOrAsync[OrganizationRole]: ... - - def list_organization_roles( - self, organization_id: str - ) -> SyncOrAsync[RoleList]: ... - - def get_organization_role( - self, organization_id: str, slug: str - ) -> SyncOrAsync[Role]: ... - - def update_organization_role( - self, - organization_id: str, - slug: str, - *, - name: Optional[str] = None, - description: Optional[str] = None, - ) -> SyncOrAsync[OrganizationRole]: ... - - def set_organization_role_permissions( - self, - organization_id: str, - slug: str, - *, - permissions: Sequence[str], - ) -> SyncOrAsync[OrganizationRole]: ... - - def add_organization_role_permission( - self, - organization_id: str, - slug: str, - *, - permission_slug: str, - ) -> SyncOrAsync[OrganizationRole]: ... - - def remove_organization_role_permission( - self, - organization_id: str, - slug: str, - *, - permission_slug: str, - ) -> SyncOrAsync[None]: ... - - # Environment Roles - - def create_environment_role( - self, - *, - slug: str, - name: str, - description: Optional[str] = None, - resource_type_slug: Optional[str] = None, - ) -> SyncOrAsync[EnvironmentRole]: ... - - def list_environment_roles(self) -> SyncOrAsync[EnvironmentRoleList]: ... - - def get_environment_role(self, slug: str) -> SyncOrAsync[EnvironmentRole]: ... - - def update_environment_role( - self, - slug: str, - *, - name: Optional[str] = None, - description: Optional[str] = None, - ) -> SyncOrAsync[EnvironmentRole]: ... - - def set_environment_role_permissions( - self, - slug: str, - *, - permissions: Sequence[str], - ) -> SyncOrAsync[EnvironmentRole]: ... - - def add_environment_role_permission( - self, - slug: str, - *, - permission_slug: str, - ) -> SyncOrAsync[EnvironmentRole]: ... - - def get_resource(self, resource_id: str) -> SyncOrAsync[AuthorizationResource]: ... - - def create_resource( - self, - *, - external_id: str, - name: str, - description: Optional[str] = None, - resource_type_slug: str, - organization_id: str, - parent: Optional[ParentResourceIdentifier] = None, - ) -> SyncOrAsync[AuthorizationResource]: ... - - def update_resource( - self, - resource_id: str, - *, - name: Optional[str] = None, - description: Union[str, None, _Unset] = UNSET, - ) -> SyncOrAsync[AuthorizationResource]: ... - - def delete_resource( - self, - resource_id: str, - *, - cascade_delete: Optional[bool] = None, - ) -> SyncOrAsync[None]: ... - - def list_resources( - self, - *, - organization_id: Optional[str] = None, - resource_type_slug: Optional[str] = None, - parent_resource_id: Optional[str] = None, - parent_resource_type_slug: Optional[str] = None, - parent_external_id: Optional[str] = None, - search: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[AuthorizationResourcesList]: ... - - def get_resource_by_external_id( - self, - organization_id: str, - resource_type: str, - external_id: str, - ) -> SyncOrAsync[AuthorizationResource]: ... - - def update_resource_by_external_id( - self, - organization_id: str, - resource_type: str, - external_id: str, - *, - name: Optional[str] = None, - description: Union[str, None, _Unset] = UNSET, - ) -> SyncOrAsync[AuthorizationResource]: ... - - def delete_resource_by_external_id( - self, - organization_id: str, - resource_type: str, - external_id: str, - *, - cascade_delete: Optional[bool] = None, - ) -> SyncOrAsync[None]: ... - - def check( - self, - organization_membership_id: str, - *, - permission_slug: str, - resource: ResourceIdentifier, - ) -> SyncOrAsync[AccessCheckResponse]: ... - - def assign_role( - self, - organization_membership_id: str, - *, - role_slug: str, - resource_identifier: ResourceIdentifier, - ) -> SyncOrAsync[RoleAssignment]: ... - - def remove_role( - self, - organization_membership_id: str, - *, - role_slug: str, - resource_identifier: ResourceIdentifier, - ) -> SyncOrAsync[None]: ... - - def remove_role_assignment( - self, - organization_membership_id: str, - role_assignment_id: str, - ) -> SyncOrAsync[None]: ... - - def list_role_assignments( - self, - *, - organization_membership_id: str, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[RoleAssignmentsListResource]: ... - - def list_resources_for_membership( - self, - organization_membership_id: str, - *, - permission_slug: str, - parent_resource: ParentResourceIdentifier, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[AuthorizationResourcesForMembershipList]: ... - - def list_memberships_for_resource( - self, - resource_id: str, - *, - permission_slug: str, - assignment: Optional[Assignment] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[AuthorizationOrganizationMembershipList]: ... - - def list_memberships_for_resource_by_external_id( - self, - organization_id: str, - resource_type_slug: str, - external_id: str, - *, - permission_slug: str, - assignment: Optional[Assignment] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[AuthorizationOrganizationMembershipList]: ... - - -class Authorization(AuthorizationModule): - _http_client: SyncHTTPClient - - def __init__(self, http_client: SyncHTTPClient): - self._http_client = http_client - - def create_permission( - self, - *, - slug: str, - name: str, - description: Optional[str] = None, - resource_type_slug: Optional[str] = None, - ) -> Permission: - json: Dict[str, Any] = {"slug": slug, "name": name} - if description is not None: - json["description"] = description - if resource_type_slug is not None: - json["resource_type_slug"] = resource_type_slug - - response = self._http_client.request( - AUTHORIZATION_PERMISSIONS_PATH, - method=REQUEST_METHOD_POST, - json=json, - ) - - return Permission.model_validate(response) - - def list_permissions( - self, - *, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> PermissionsListResource: - list_params: PermissionListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = self._http_client.request( - AUTHORIZATION_PERMISSIONS_PATH, - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return WorkOSListResource[Permission, PermissionListFilters, ListMetadata]( - list_method=self.list_permissions, - list_args=list_params, - **ListPage[Permission](**response).model_dump(), - ) - - def get_permission(self, slug: str) -> Permission: - response = self._http_client.request( - f"{AUTHORIZATION_PERMISSIONS_PATH}/{slug}", - method=REQUEST_METHOD_GET, - ) - - return Permission.model_validate(response) - - def update_permission( - self, - slug: str, - *, - name: Optional[str] = None, - description: Optional[str] = None, - ) -> Permission: - json: Dict[str, Any] = {} - if name is not None: - json["name"] = name - if description is not None: - json["description"] = description - - response = self._http_client.request( - f"{AUTHORIZATION_PERMISSIONS_PATH}/{slug}", - method=REQUEST_METHOD_PATCH, - json=json, - ) - - return Permission.model_validate(response) - - def delete_permission(self, slug: str) -> None: - self._http_client.request( - f"{AUTHORIZATION_PERMISSIONS_PATH}/{slug}", - method=REQUEST_METHOD_DELETE, - ) - - # Organization Roles - - def create_organization_role( - self, - organization_id: str, - *, - slug: str, - name: str, - description: Optional[str] = None, - ) -> OrganizationRole: - json: Dict[str, Any] = {"slug": slug, "name": name} - if description is not None: - json["description"] = description - - response = self._http_client.request( - f"authorization/organizations/{organization_id}/roles", - method=REQUEST_METHOD_POST, - json=json, - ) - - return OrganizationRole.model_validate(response) - - def list_organization_roles(self, organization_id: str) -> RoleList: - response = self._http_client.request( - f"authorization/organizations/{organization_id}/roles", - method=REQUEST_METHOD_GET, - ) - - return RoleList.model_validate(response) - - def get_organization_role(self, organization_id: str, slug: str) -> Role: - response = self._http_client.request( - f"authorization/organizations/{organization_id}/roles/{slug}", - method=REQUEST_METHOD_GET, - ) - - return _role_adapter.validate_python(response) - - def update_organization_role( - self, - organization_id: str, - slug: str, - *, - name: Optional[str] = None, - description: Optional[str] = None, - ) -> OrganizationRole: - json: Dict[str, Any] = {} - if name is not None: - json["name"] = name - if description is not None: - json["description"] = description - - response = self._http_client.request( - f"authorization/organizations/{organization_id}/roles/{slug}", - method=REQUEST_METHOD_PATCH, - json=json, - ) - - return OrganizationRole.model_validate(response) - - def set_organization_role_permissions( - self, - organization_id: str, - slug: str, - *, - permissions: Sequence[str], - ) -> OrganizationRole: - response = self._http_client.request( - f"authorization/organizations/{organization_id}/roles/{slug}/permissions", - method=REQUEST_METHOD_PUT, - json={"permissions": list(permissions)}, - ) - - return OrganizationRole.model_validate(response) - - def add_organization_role_permission( - self, - organization_id: str, - slug: str, - *, - permission_slug: str, - ) -> OrganizationRole: - response = self._http_client.request( - f"authorization/organizations/{organization_id}/roles/{slug}/permissions", - method=REQUEST_METHOD_POST, - json={"slug": permission_slug}, - ) - - return OrganizationRole.model_validate(response) - - def remove_organization_role_permission( - self, - organization_id: str, - slug: str, - *, - permission_slug: str, - ) -> None: - self._http_client.request( - f"authorization/organizations/{organization_id}/roles/{slug}/permissions/{permission_slug}", - method=REQUEST_METHOD_DELETE, - ) - - # Environment Roles - - def create_environment_role( - self, - *, - slug: str, - name: str, - description: Optional[str] = None, - resource_type_slug: Optional[str] = None, - ) -> EnvironmentRole: - json: Dict[str, Any] = {"slug": slug, "name": name} - if description is not None: - json["description"] = description - if resource_type_slug is not None: - json["resource_type_slug"] = resource_type_slug - - response = self._http_client.request( - "authorization/roles", - method=REQUEST_METHOD_POST, - json=json, - ) - - return EnvironmentRole.model_validate(response) - - def list_environment_roles(self) -> EnvironmentRoleList: - response = self._http_client.request( - "authorization/roles", - method=REQUEST_METHOD_GET, - ) - - return EnvironmentRoleList.model_validate(response) - - def get_environment_role(self, slug: str) -> EnvironmentRole: - response = self._http_client.request( - f"authorization/roles/{slug}", - method=REQUEST_METHOD_GET, - ) - - return EnvironmentRole.model_validate(response) - - def update_environment_role( - self, - slug: str, - *, - name: Optional[str] = None, - description: Optional[str] = None, - ) -> EnvironmentRole: - json: Dict[str, Any] = {} - if name is not None: - json["name"] = name - if description is not None: - json["description"] = description - - response = self._http_client.request( - f"authorization/roles/{slug}", - method=REQUEST_METHOD_PATCH, - json=json, - ) - - return EnvironmentRole.model_validate(response) - - def set_environment_role_permissions( - self, - slug: str, - *, - permissions: Sequence[str], - ) -> EnvironmentRole: - response = self._http_client.request( - f"authorization/roles/{slug}/permissions", - method=REQUEST_METHOD_PUT, - json={"permissions": list(permissions)}, - ) - - return EnvironmentRole.model_validate(response) - - def add_environment_role_permission( - self, - slug: str, - *, - permission_slug: str, - ) -> EnvironmentRole: - response = self._http_client.request( - f"authorization/roles/{slug}/permissions", - method=REQUEST_METHOD_POST, - json={"slug": permission_slug}, - ) - - return EnvironmentRole.model_validate(response) - - def get_resource(self, resource_id: str) -> AuthorizationResource: - response = self._http_client.request( - f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", - method=REQUEST_METHOD_GET, - ) - - return AuthorizationResource.model_validate(response) - - def create_resource( - self, - *, - external_id: str, - name: str, - description: Optional[str] = None, - resource_type_slug: str, - organization_id: str, - parent: Optional[ParentResourceIdentifier] = None, - ) -> AuthorizationResource: - json: Dict[str, Any] = { - "resource_type_slug": resource_type_slug, - "organization_id": organization_id, - "external_id": external_id, - "name": name, - } - if parent is not None: - json.update(parent) - if description is not None: - json["description"] = description - - response = self._http_client.request( - AUTHORIZATION_RESOURCES_PATH, - method=REQUEST_METHOD_POST, - json=json, - ) - - return AuthorizationResource.model_validate(response) - - def update_resource( - self, - resource_id: str, - *, - name: Optional[str] = None, - description: Union[str, None, _Unset] = UNSET, - ) -> AuthorizationResource: - json: Dict[str, Any] = {} - if name is not None: - json["name"] = name - if not isinstance(description, _Unset): - json["description"] = description - - response = self._http_client.request( - f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", - method=REQUEST_METHOD_PATCH, - json=json, - exclude_none=False, - ) - - return AuthorizationResource.model_validate(response) - - def delete_resource( - self, - resource_id: str, - *, - cascade_delete: Optional[bool] = None, - ) -> None: - params = ( - {"cascade_delete": str(cascade_delete).lower()} - if cascade_delete is not None - else None - ) - self._http_client.request( - f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", - method=REQUEST_METHOD_DELETE, - params=params, - ) - - def list_resources( - self, - *, - organization_id: Optional[str] = None, - resource_type_slug: Optional[str] = None, - parent_resource_id: Optional[str] = None, - parent_resource_type_slug: Optional[str] = None, - parent_external_id: Optional[str] = None, - search: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> AuthorizationResourcesList: - list_params: ResourceListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - if organization_id is not None: - list_params["organization_id"] = organization_id - if resource_type_slug is not None: - list_params["resource_type_slug"] = resource_type_slug - if parent_resource_id is not None: - list_params["parent_resource_id"] = parent_resource_id - if parent_resource_type_slug is not None: - list_params["parent_resource_type_slug"] = parent_resource_type_slug - if parent_external_id is not None: - list_params["parent_external_id"] = parent_external_id - if search is not None: - list_params["search"] = search - - response = self._http_client.request( - AUTHORIZATION_RESOURCES_PATH, - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return WorkOSListResource[ - AuthorizationResource, ResourceListFilters, ListMetadata - ]( - list_method=self.list_resources, - list_args=list_params, - **ListPage[AuthorizationResource](**response).model_dump(), - ) - - def get_resource_by_external_id( - self, - organization_id: str, - resource_type: str, - external_id: str, - ) -> AuthorizationResource: - response = self._http_client.request( - f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type}/{external_id}", - method=REQUEST_METHOD_GET, - ) - - return AuthorizationResource.model_validate(response) - - def update_resource_by_external_id( - self, - organization_id: str, - resource_type: str, - external_id: str, - *, - name: Optional[str] = None, - description: Union[str, None, _Unset] = UNSET, - ) -> AuthorizationResource: - json: Dict[str, Any] = {} - if name is not None: - json["name"] = name - if not isinstance(description, _Unset): - json["description"] = description - - response = self._http_client.request( - f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type}/{external_id}", - method=REQUEST_METHOD_PATCH, - json=json, - exclude_none=False, - ) - - return AuthorizationResource.model_validate(response) - - def delete_resource_by_external_id( - self, - organization_id: str, - resource_type: str, - external_id: str, - *, - cascade_delete: Optional[bool] = None, - ) -> None: - path = f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type}/{external_id}" - params = ( - {"cascade_delete": str(cascade_delete).lower()} - if cascade_delete is not None - else None - ) - self._http_client.request( - path, - method=REQUEST_METHOD_DELETE, - params=params, - ) - - def check( - self, - organization_membership_id: str, - *, - permission_slug: str, - resource: ResourceIdentifier, - ) -> AccessCheckResponse: - json: Dict[str, Any] = {"permission_slug": permission_slug} - json.update(resource) - - response = self._http_client.request( - f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/check", - method=REQUEST_METHOD_POST, - json=json, - ) - - return AccessCheckResponse.model_validate(response) - - def assign_role( - self, - organization_membership_id: str, - *, - role_slug: str, - resource_identifier: ResourceIdentifier, - ) -> RoleAssignment: - json: Dict[str, Any] = {"role_slug": role_slug} - json.update(resource_identifier) - - response = self._http_client.request( - f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/role_assignments", - method=REQUEST_METHOD_POST, - json=json, - ) - - return RoleAssignment.model_validate(response) - - def remove_role( - self, - organization_membership_id: str, - *, - role_slug: str, - resource_identifier: ResourceIdentifier, - ) -> None: - json: Dict[str, Any] = {"role_slug": role_slug} - json.update(resource_identifier) - - self._http_client.delete_with_body( - f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/role_assignments", - json=json, - ) - - def remove_role_assignment( - self, - organization_membership_id: str, - role_assignment_id: str, - ) -> None: - self._http_client.request( - f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/role_assignments/{role_assignment_id}", - method=REQUEST_METHOD_DELETE, - ) - - def list_role_assignments( - self, - *, - organization_membership_id: str, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> RoleAssignmentsListResource: - list_params: RoleAssignmentListFilters = { - "organization_membership_id": organization_membership_id, - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - query_params: ListArgs = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = self._http_client.request( - f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/role_assignments", - method=REQUEST_METHOD_GET, - params=query_params, - ) - - return WorkOSListResource[ - RoleAssignment, RoleAssignmentListFilters, ListMetadata - ]( - list_method=self.list_role_assignments, - list_args=list_params, - **ListPage[RoleAssignment](**response).model_dump(), - ) - - def list_resources_for_membership( - self, - organization_membership_id: str, - *, - permission_slug: str, - parent_resource: ParentResourceIdentifier, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> AuthorizationResourcesForMembershipList: - list_params: ResourcesForMembershipListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - "permission_slug": permission_slug, - } - - http_params: Dict[str, Any] = {**list_params} - http_params.update(parent_resource) - - response = self._http_client.request( - f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/resources", - method=REQUEST_METHOD_GET, - params=http_params, - ) - - return AuthorizationResourcesForMembershipList( - list_method=partial( - self.list_resources_for_membership, - organization_membership_id, - parent_resource=parent_resource, - ), - list_args=list_params, - **ListPage[AuthorizationResource](**response).model_dump(), - ) - - def list_memberships_for_resource( - self, - resource_id: str, - *, - permission_slug: str, - assignment: Optional[Assignment] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> AuthorizationOrganizationMembershipList: - list_params: AuthorizationOrganizationMembershipListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - "permission_slug": permission_slug, - } - if assignment is not None: - list_params["assignment"] = assignment - - response = self._http_client.request( - f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}/organization_memberships", - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return WorkOSListResource[ - AuthorizationOrganizationMembership, - AuthorizationOrganizationMembershipListFilters, - ListMetadata, - ]( - list_method=partial(self.list_memberships_for_resource, resource_id), - list_args=list_params, - **ListPage[AuthorizationOrganizationMembership](**response).model_dump(), - ) - - def list_memberships_for_resource_by_external_id( - self, - organization_id: str, - resource_type_slug: str, - external_id: str, - *, - permission_slug: str, - assignment: Optional[Assignment] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> AuthorizationOrganizationMembershipList: - list_params: AuthorizationOrganizationMembershipListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - "permission_slug": permission_slug, - } - if assignment is not None: - list_params["assignment"] = assignment - - response = self._http_client.request( - f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type_slug}/{external_id}/organization_memberships", - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return WorkOSListResource[ - AuthorizationOrganizationMembership, - AuthorizationOrganizationMembershipListFilters, - ListMetadata, - ]( - list_method=partial( - self.list_memberships_for_resource_by_external_id, - organization_id, - resource_type_slug, - external_id, - ), - list_args=list_params, - **ListPage[AuthorizationOrganizationMembership](**response).model_dump(), - ) - - -class AsyncAuthorization(AuthorizationModule): - _http_client: AsyncHTTPClient - - def __init__(self, http_client: AsyncHTTPClient): - self._http_client = http_client - - async def create_permission( - self, - *, - slug: str, - name: str, - description: Optional[str] = None, - resource_type_slug: Optional[str] = None, - ) -> Permission: - json: Dict[str, Any] = {"slug": slug, "name": name} - if description is not None: - json["description"] = description - if resource_type_slug is not None: - json["resource_type_slug"] = resource_type_slug - - response = await self._http_client.request( - AUTHORIZATION_PERMISSIONS_PATH, - method=REQUEST_METHOD_POST, - json=json, - ) - - return Permission.model_validate(response) - - async def list_permissions( - self, - *, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> PermissionsListResource: - list_params: PermissionListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = await self._http_client.request( - AUTHORIZATION_PERMISSIONS_PATH, - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return WorkOSListResource[Permission, PermissionListFilters, ListMetadata]( - list_method=self.list_permissions, - list_args=list_params, - **ListPage[Permission](**response).model_dump(), - ) - - async def get_permission(self, slug: str) -> Permission: - response = await self._http_client.request( - f"{AUTHORIZATION_PERMISSIONS_PATH}/{slug}", - method=REQUEST_METHOD_GET, - ) - - return Permission.model_validate(response) - - async def update_permission( - self, - slug: str, - *, - name: Optional[str] = None, - description: Optional[str] = None, - ) -> Permission: - json: Dict[str, Any] = {} - if name is not None: - json["name"] = name - if description is not None: - json["description"] = description - - response = await self._http_client.request( - f"{AUTHORIZATION_PERMISSIONS_PATH}/{slug}", - method=REQUEST_METHOD_PATCH, - json=json, - ) - - return Permission.model_validate(response) - - async def delete_permission(self, slug: str) -> None: - await self._http_client.request( - f"{AUTHORIZATION_PERMISSIONS_PATH}/{slug}", - method=REQUEST_METHOD_DELETE, - ) - - async def create_organization_role( - self, - organization_id: str, - *, - slug: str, - name: str, - description: Optional[str] = None, - ) -> OrganizationRole: - json: Dict[str, Any] = {"slug": slug, "name": name} - if description is not None: - json["description"] = description - - response = await self._http_client.request( - f"authorization/organizations/{organization_id}/roles", - method=REQUEST_METHOD_POST, - json=json, - ) - - return OrganizationRole.model_validate(response) - - async def list_organization_roles(self, organization_id: str) -> RoleList: - response = await self._http_client.request( - f"authorization/organizations/{organization_id}/roles", - method=REQUEST_METHOD_GET, - ) - - return RoleList.model_validate(response) - - async def get_organization_role(self, organization_id: str, slug: str) -> Role: - response = await self._http_client.request( - f"authorization/organizations/{organization_id}/roles/{slug}", - method=REQUEST_METHOD_GET, - ) - - return _role_adapter.validate_python(response) - - async def update_organization_role( - self, - organization_id: str, - slug: str, - *, - name: Optional[str] = None, - description: Optional[str] = None, - ) -> OrganizationRole: - json: Dict[str, Any] = {} - if name is not None: - json["name"] = name - if description is not None: - json["description"] = description - - response = await self._http_client.request( - f"authorization/organizations/{organization_id}/roles/{slug}", - method=REQUEST_METHOD_PATCH, - json=json, - ) - - return OrganizationRole.model_validate(response) - - async def set_organization_role_permissions( - self, - organization_id: str, - slug: str, - *, - permissions: Sequence[str], - ) -> OrganizationRole: - response = await self._http_client.request( - f"authorization/organizations/{organization_id}/roles/{slug}/permissions", - method=REQUEST_METHOD_PUT, - json={"permissions": list(permissions)}, - ) - - return OrganizationRole.model_validate(response) - - async def add_organization_role_permission( - self, - organization_id: str, - slug: str, - *, - permission_slug: str, - ) -> OrganizationRole: - response = await self._http_client.request( - f"authorization/organizations/{organization_id}/roles/{slug}/permissions", - method=REQUEST_METHOD_POST, - json={"slug": permission_slug}, - ) - - return OrganizationRole.model_validate(response) - - async def remove_organization_role_permission( - self, - organization_id: str, - slug: str, - *, - permission_slug: str, - ) -> None: - await self._http_client.request( - f"authorization/organizations/{organization_id}/roles/{slug}/permissions/{permission_slug}", - method=REQUEST_METHOD_DELETE, - ) - - async def create_environment_role( - self, - *, - slug: str, - name: str, - description: Optional[str] = None, - resource_type_slug: Optional[str] = None, - ) -> EnvironmentRole: - json: Dict[str, Any] = {"slug": slug, "name": name} - if description is not None: - json["description"] = description - if resource_type_slug is not None: - json["resource_type_slug"] = resource_type_slug - - response = await self._http_client.request( - "authorization/roles", - method=REQUEST_METHOD_POST, - json=json, - ) - - return EnvironmentRole.model_validate(response) - - async def list_environment_roles(self) -> EnvironmentRoleList: - response = await self._http_client.request( - "authorization/roles", - method=REQUEST_METHOD_GET, - ) - - return EnvironmentRoleList.model_validate(response) - - async def get_environment_role(self, slug: str) -> EnvironmentRole: - response = await self._http_client.request( - f"authorization/roles/{slug}", - method=REQUEST_METHOD_GET, - ) - - return EnvironmentRole.model_validate(response) - - async def update_environment_role( - self, - slug: str, - *, - name: Optional[str] = None, - description: Optional[str] = None, - ) -> EnvironmentRole: - json: Dict[str, Any] = {} - if name is not None: - json["name"] = name - if description is not None: - json["description"] = description - - response = await self._http_client.request( - f"authorization/roles/{slug}", - method=REQUEST_METHOD_PATCH, - json=json, - ) - - return EnvironmentRole.model_validate(response) - - async def set_environment_role_permissions( - self, - slug: str, - *, - permissions: Sequence[str], - ) -> EnvironmentRole: - response = await self._http_client.request( - f"authorization/roles/{slug}/permissions", - method=REQUEST_METHOD_PUT, - json={"permissions": list(permissions)}, - ) - - return EnvironmentRole.model_validate(response) - - async def add_environment_role_permission( - self, - slug: str, - *, - permission_slug: str, - ) -> EnvironmentRole: - response = await self._http_client.request( - f"authorization/roles/{slug}/permissions", - method=REQUEST_METHOD_POST, - json={"slug": permission_slug}, - ) - - return EnvironmentRole.model_validate(response) - - async def get_resource(self, resource_id: str) -> AuthorizationResource: - response = await self._http_client.request( - f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", - method=REQUEST_METHOD_GET, - ) - - return AuthorizationResource.model_validate(response) - - async def create_resource( - self, - *, - external_id: str, - name: str, - description: Optional[str] = None, - resource_type_slug: str, - organization_id: str, - parent: Optional[ParentResourceIdentifier] = None, - ) -> AuthorizationResource: - json: Dict[str, Any] = { - "resource_type_slug": resource_type_slug, - "organization_id": organization_id, - "external_id": external_id, - "name": name, - } - if parent is not None: - json.update(parent) - if description is not None: - json["description"] = description - - response = await self._http_client.request( - AUTHORIZATION_RESOURCES_PATH, - method=REQUEST_METHOD_POST, - json=json, - ) - - return AuthorizationResource.model_validate(response) - - async def update_resource( - self, - resource_id: str, - *, - name: Optional[str] = None, - description: Union[str, None, _Unset] = UNSET, - ) -> AuthorizationResource: - json: Dict[str, Any] = {} - if name is not None: - json["name"] = name - if not isinstance(description, _Unset): - json["description"] = description - - response = await self._http_client.request( - f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", - method=REQUEST_METHOD_PATCH, - json=json, - exclude_none=False, - ) - - return AuthorizationResource.model_validate(response) - - async def delete_resource( - self, - resource_id: str, - *, - cascade_delete: Optional[bool] = None, - ) -> None: - params = ( - {"cascade_delete": str(cascade_delete).lower()} - if cascade_delete is not None - else None - ) - await self._http_client.request( - f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}", - method=REQUEST_METHOD_DELETE, - params=params, - ) - - async def list_resources( - self, - *, - organization_id: Optional[str] = None, - resource_type_slug: Optional[str] = None, - parent_resource_id: Optional[str] = None, - parent_resource_type_slug: Optional[str] = None, - parent_external_id: Optional[str] = None, - search: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> AuthorizationResourcesList: - list_params: ResourceListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - if organization_id is not None: - list_params["organization_id"] = organization_id - if resource_type_slug is not None: - list_params["resource_type_slug"] = resource_type_slug - if parent_resource_id is not None: - list_params["parent_resource_id"] = parent_resource_id - if parent_resource_type_slug is not None: - list_params["parent_resource_type_slug"] = parent_resource_type_slug - if parent_external_id is not None: - list_params["parent_external_id"] = parent_external_id - if search is not None: - list_params["search"] = search - - response = await self._http_client.request( - AUTHORIZATION_RESOURCES_PATH, - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return WorkOSListResource[ - AuthorizationResource, ResourceListFilters, ListMetadata - ]( - list_method=self.list_resources, - list_args=list_params, - **ListPage[AuthorizationResource](**response).model_dump(), - ) - - async def get_resource_by_external_id( - self, - organization_id: str, - resource_type: str, - external_id: str, - ) -> AuthorizationResource: - response = await self._http_client.request( - f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type}/{external_id}", - method=REQUEST_METHOD_GET, - ) - - return AuthorizationResource.model_validate(response) - - async def update_resource_by_external_id( - self, - organization_id: str, - resource_type: str, - external_id: str, - *, - name: Optional[str] = None, - description: Union[str, None, _Unset] = UNSET, - ) -> AuthorizationResource: - json: Dict[str, Any] = {} - if name is not None: - json["name"] = name - if not isinstance(description, _Unset): - json["description"] = description - - response = await self._http_client.request( - f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type}/{external_id}", - method=REQUEST_METHOD_PATCH, - json=json, - exclude_none=False, - ) - - return AuthorizationResource.model_validate(response) - - async def delete_resource_by_external_id( - self, - organization_id: str, - resource_type: str, - external_id: str, - *, - cascade_delete: Optional[bool] = None, - ) -> None: - path = f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type}/{external_id}" - params = ( - {"cascade_delete": str(cascade_delete).lower()} - if cascade_delete is not None - else None - ) - await self._http_client.request( - path, - method=REQUEST_METHOD_DELETE, - params=params, - ) - - async def check( - self, - organization_membership_id: str, - *, - permission_slug: str, - resource: ResourceIdentifier, - ) -> AccessCheckResponse: - json: Dict[str, Any] = {"permission_slug": permission_slug} - json.update(resource) - - response = await self._http_client.request( - f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/check", - method=REQUEST_METHOD_POST, - json=json, - ) - - return AccessCheckResponse.model_validate(response) - - async def assign_role( - self, - organization_membership_id: str, - *, - role_slug: str, - resource_identifier: ResourceIdentifier, - ) -> RoleAssignment: - json: Dict[str, Any] = {"role_slug": role_slug} - json.update(resource_identifier) - - response = await self._http_client.request( - f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/role_assignments", - method=REQUEST_METHOD_POST, - json=json, - ) - - return RoleAssignment.model_validate(response) - - async def remove_role( - self, - organization_membership_id: str, - *, - role_slug: str, - resource_identifier: ResourceIdentifier, - ) -> None: - json: Dict[str, Any] = {"role_slug": role_slug} - json.update(resource_identifier) - - await self._http_client.delete_with_body( - f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/role_assignments", - json=json, - ) - - async def remove_role_assignment( - self, - organization_membership_id: str, - role_assignment_id: str, - ) -> None: - await self._http_client.request( - f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/role_assignments/{role_assignment_id}", - method=REQUEST_METHOD_DELETE, - ) - - async def list_role_assignments( - self, - *, - organization_membership_id: str, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> RoleAssignmentsListResource: - list_params: RoleAssignmentListFilters = { - "organization_membership_id": organization_membership_id, - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - query_params: ListArgs = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = await self._http_client.request( - f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/role_assignments", - method=REQUEST_METHOD_GET, - params=query_params, - ) - - return WorkOSListResource[ - RoleAssignment, RoleAssignmentListFilters, ListMetadata - ]( - list_method=self.list_role_assignments, - list_args=list_params, - **ListPage[RoleAssignment](**response).model_dump(), - ) - - async def list_resources_for_membership( - self, - organization_membership_id: str, - *, - permission_slug: str, - parent_resource: ParentResourceIdentifier, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> AuthorizationResourcesForMembershipList: - list_params: ResourcesForMembershipListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - "permission_slug": permission_slug, - } - - http_params: Dict[str, Any] = {**list_params} - http_params.update(parent_resource) - - response = await self._http_client.request( - f"{AUTHORIZATION_ORGANIZATION_MEMBERSHIPS_PATH}/{organization_membership_id}/resources", - method=REQUEST_METHOD_GET, - params=http_params, - ) - - return AuthorizationResourcesForMembershipList( - list_method=partial( - self.list_resources_for_membership, - organization_membership_id, - parent_resource=parent_resource, - ), - list_args=list_params, - **ListPage[AuthorizationResource](**response).model_dump(), - ) - - async def list_memberships_for_resource( - self, - resource_id: str, - *, - permission_slug: str, - assignment: Optional[Assignment] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> AuthorizationOrganizationMembershipList: - list_params: AuthorizationOrganizationMembershipListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - "permission_slug": permission_slug, - } - if assignment is not None: - list_params["assignment"] = assignment - - response = await self._http_client.request( - f"{AUTHORIZATION_RESOURCES_PATH}/{resource_id}/organization_memberships", - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return WorkOSListResource[ - AuthorizationOrganizationMembership, - AuthorizationOrganizationMembershipListFilters, - ListMetadata, - ]( - list_method=partial(self.list_memberships_for_resource, resource_id), - list_args=list_params, - **ListPage[AuthorizationOrganizationMembership](**response).model_dump(), - ) - - async def list_memberships_for_resource_by_external_id( - self, - organization_id: str, - resource_type_slug: str, - external_id: str, - *, - permission_slug: str, - assignment: Optional[Assignment] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> AuthorizationOrganizationMembershipList: - list_params: AuthorizationOrganizationMembershipListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - "permission_slug": permission_slug, - } - if assignment is not None: - list_params["assignment"] = assignment - - response = await self._http_client.request( - f"{AUTHORIZATION_ORGANIZATIONS_PATH}/{organization_id}/resources/{resource_type_slug}/{external_id}/organization_memberships", - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return WorkOSListResource[ - AuthorizationOrganizationMembership, - AuthorizationOrganizationMembershipListFilters, - ListMetadata, - ]( - list_method=partial( - self.list_memberships_for_resource_by_external_id, - organization_id, - resource_type_slug, - external_id, - ), - list_args=list_params, - **ListPage[AuthorizationOrganizationMembership](**response).model_dump(), - ) diff --git a/src/workos/client.py b/src/workos/client.py deleted file mode 100644 index 7e942924..00000000 --- a/src/workos/client.py +++ /dev/null @@ -1,164 +0,0 @@ -from importlib.metadata import version -from typing import Optional -from workos._base_client import BaseClient -from workos.api_keys import ApiKeys -from workos.audit_logs import AuditLogs -from workos.authorization import Authorization -from workos.connect import Connect -from workos.directory_sync import DirectorySync -from workos.fga import FGA -from workos.organizations import Organizations -from workos.organization_domains import OrganizationDomains -from workos.passwordless import Passwordless -from workos.pipes import Pipes -from workos.portal import Portal -from workos.sso import SSO -from workos.webhooks import Webhooks -from workos.mfa import Mfa -from workos.events import Events -from workos.user_management import UserManagement -from workos.utils.http_client import SyncHTTPClient -from workos.widgets import Widgets -from workos.vault import Vault - - -class SyncClient(BaseClient): - """Client for a convenient way to access the WorkOS feature set.""" - - _http_client: SyncHTTPClient - - def __init__( - self, - *, - api_key: Optional[str] = None, - client_id: Optional[str] = None, - base_url: Optional[str] = None, - request_timeout: Optional[int] = None, - jwt_leeway: float = 0, - ): - super().__init__( - api_key=api_key, - client_id=client_id, - base_url=base_url, - request_timeout=request_timeout, - jwt_leeway=jwt_leeway, - ) - self._http_client = SyncHTTPClient( - api_key=self._api_key, - base_url=self.base_url, - client_id=self._client_id, - version=version("workos"), - timeout=self.request_timeout, - ) - - @property - def connect(self) -> Connect: - if not getattr(self, "_connect", None): - self._connect = Connect(self._http_client) - return self._connect - - @property - def api_keys(self) -> ApiKeys: - if not getattr(self, "_api_keys", None): - self._api_keys = ApiKeys(self._http_client) - return self._api_keys - - @property - def authorization(self) -> Authorization: - if not getattr(self, "_authorization", None): - self._authorization = Authorization(self._http_client) - return self._authorization - - @property - def sso(self) -> SSO: - if not getattr(self, "_sso", None): - self._sso = SSO(http_client=self._http_client, client_configuration=self) - return self._sso - - @property - def audit_logs(self) -> AuditLogs: - if not getattr(self, "_audit_logs", None): - self._audit_logs = AuditLogs(self._http_client) - return self._audit_logs - - @property - def directory_sync(self) -> DirectorySync: - if not getattr(self, "_directory_sync", None): - self._directory_sync = DirectorySync(self._http_client) - return self._directory_sync - - @property - def events(self) -> Events: - if not getattr(self, "_events", None): - self._events = Events(self._http_client) - return self._events - - @property - def fga(self) -> FGA: - if not getattr(self, "_fga", None): - self._fga = FGA(self._http_client) - return self._fga - - @property - def organizations(self) -> Organizations: - if not getattr(self, "_organizations", None): - self._organizations = Organizations(self._http_client) - return self._organizations - - @property - def organization_domains(self) -> OrganizationDomains: - if not getattr(self, "_organization_domains", None): - self._organization_domains = OrganizationDomains( - http_client=self._http_client, client_configuration=self - ) - return self._organization_domains - - @property - def passwordless(self) -> Passwordless: - if not getattr(self, "_passwordless", None): - self._passwordless = Passwordless(self._http_client) - return self._passwordless - - @property - def pipes(self) -> Pipes: - if not getattr(self, "_pipes", None): - self._pipes = Pipes(self._http_client) - return self._pipes - - @property - def portal(self) -> Portal: - if not getattr(self, "_portal", None): - self._portal = Portal(self._http_client) - return self._portal - - @property - def webhooks(self) -> Webhooks: - if not getattr(self, "_webhooks", None): - self._webhooks = Webhooks() - return self._webhooks - - @property - def mfa(self) -> Mfa: - if not getattr(self, "_mfa", None): - self._mfa = Mfa(self._http_client) - return self._mfa - - @property - def user_management(self) -> UserManagement: - if not getattr(self, "_user_management", None): - self._user_management = UserManagement( - http_client=self._http_client, client_configuration=self - ) - return self._user_management - - @property - def widgets(self) -> Widgets: - if not getattr(self, "_widgets", None): - self._widgets = Widgets(http_client=self._http_client) - return self._widgets - - @property - def vault(self) -> Vault: - if not getattr(self, "_vault", None): - self._vault = Vault(http_client=self._http_client) - return self._vault diff --git a/src/workos/connect.py b/src/workos/connect.py deleted file mode 100644 index f8b73bf2..00000000 --- a/src/workos/connect.py +++ /dev/null @@ -1,427 +0,0 @@ -from typing import Optional, Protocol, Sequence - -from workos.types.connect import ClientSecret, ConnectApplication -from workos.types.connect.connect_application import ApplicationType -from workos.types.connect.list_filters import ConnectApplicationListFilters -from workos.types.connect.redirect_uri_input import RedirectUriInput -from workos.types.list_resource import ListMetadata, ListPage, WorkOSListResource -from workos.typing.sync_or_async import SyncOrAsync -from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient -from workos.utils.pagination_order import PaginationOrder -from workos.utils.request_helper import ( - DEFAULT_LIST_RESPONSE_LIMIT, - REQUEST_METHOD_DELETE, - REQUEST_METHOD_GET, - REQUEST_METHOD_POST, - REQUEST_METHOD_PUT, -) - -CONNECT_APPLICATIONS_PATH = "connect/applications" -CONNECT_CLIENT_SECRETS_PATH = "connect/client_secrets" - -ConnectApplicationsListResource = WorkOSListResource[ - ConnectApplication, ConnectApplicationListFilters, ListMetadata -] - - -class ConnectModule(Protocol): - """Offers methods through the WorkOS Connect service.""" - - def list_applications( - self, - *, - organization_id: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[ConnectApplicationsListResource]: - """Retrieve a list of connect applications. - - Kwargs: - organization_id (str): Filter by organization ID. (Optional) - limit (int): Maximum number of records to return. (Optional) - before (str): Pagination cursor to receive records before a provided ID. (Optional) - after (str): Pagination cursor to receive records after a provided ID. (Optional) - order (Literal["asc","desc"]): Sort records in either ascending or descending order. (Optional) - - Returns: - ConnectApplicationsListResource: Applications list response from WorkOS. - """ - ... - - def get_application(self, application_id: str) -> SyncOrAsync[ConnectApplication]: - """Gets details for a single connect application. - - Args: - application_id (str): Application ID or client ID. - - Returns: - ConnectApplication: Application response from WorkOS. - """ - ... - - def create_application( - self, - *, - name: str, - application_type: ApplicationType, - is_first_party: bool, - description: Optional[str] = None, - scopes: Optional[Sequence[str]] = None, - redirect_uris: Optional[Sequence[RedirectUriInput]] = None, - uses_pkce: Optional[bool] = None, - organization_id: Optional[str] = None, - ) -> SyncOrAsync[ConnectApplication]: - """Create a connect application. - - Kwargs: - name (str): Application name. - application_type (ApplicationType): "oauth" or "m2m". - is_first_party (bool): Whether this is a first-party application. - description (str): Application description. (Optional) - scopes (Sequence[str]): Permission slugs. (Optional) - redirect_uris (Sequence[str]): OAuth redirect URIs. (Optional) - uses_pkce (bool): PKCE support (OAuth only). (Optional) - organization_id (str): Organization ID. Required for M2M and third-party OAuth. (Optional) - - Returns: - ConnectApplication: Created application response from WorkOS. - """ - ... - - def update_application( - self, - *, - application_id: str, - name: Optional[str] = None, - description: Optional[str] = None, - scopes: Optional[Sequence[str]] = None, - redirect_uris: Optional[Sequence[RedirectUriInput]] = None, - ) -> SyncOrAsync[ConnectApplication]: - """Update a connect application. - - Kwargs: - application_id (str): Application ID or client ID. - name (str): Updated application name. (Optional) - description (str): Updated description. Pass None to clear. (Optional) - scopes (Sequence[str]): Updated permission slugs. (Optional) - redirect_uris (Sequence[str]): Updated OAuth redirect URIs. (Optional) - - Returns: - ConnectApplication: Updated application response from WorkOS. - """ - ... - - def delete_application(self, application_id: str) -> SyncOrAsync[None]: - """Delete a connect application. - - Args: - application_id (str): Application ID or client ID. - - Returns: - None - """ - ... - - def create_client_secret(self, application_id: str) -> SyncOrAsync[ClientSecret]: - """Create a client secret for a connect application. - - Args: - application_id (str): Application ID or client ID. - - Returns: - ClientSecret: Created client secret response from WorkOS. - """ - ... - - def list_client_secrets( - self, - application_id: str, - ) -> SyncOrAsync[Sequence[ClientSecret]]: - """List client secrets for a connect application. - - Args: - application_id (str): Application ID or client ID. - - Returns: - Sequence[ClientSecret]: Client secrets for the application. - """ - ... - - def delete_client_secret(self, client_secret_id: str) -> SyncOrAsync[None]: - """Delete a client secret. - - Args: - client_secret_id (str): Client secret ID. - - Returns: - None - """ - ... - - -class Connect(ConnectModule): - _http_client: SyncHTTPClient - - def __init__(self, http_client: SyncHTTPClient): - self._http_client = http_client - - def list_applications( - self, - *, - organization_id: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> ConnectApplicationsListResource: - list_params: ConnectApplicationListFilters = { - "organization_id": organization_id, - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = self._http_client.request( - CONNECT_APPLICATIONS_PATH, - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return WorkOSListResource[ - ConnectApplication, ConnectApplicationListFilters, ListMetadata - ]( - list_method=self.list_applications, - list_args=list_params, - **ListPage[ConnectApplication](**response).model_dump(), - ) - - def get_application(self, application_id: str) -> ConnectApplication: - response = self._http_client.request( - f"{CONNECT_APPLICATIONS_PATH}/{application_id}", - method=REQUEST_METHOD_GET, - ) - - return ConnectApplication.model_validate(response) - - def create_application( - self, - *, - name: str, - application_type: ApplicationType, - is_first_party: bool, - description: Optional[str] = None, - scopes: Optional[Sequence[str]] = None, - redirect_uris: Optional[Sequence[RedirectUriInput]] = None, - uses_pkce: Optional[bool] = None, - organization_id: Optional[str] = None, - ) -> ConnectApplication: - json = { - "name": name, - "application_type": application_type, - "is_first_party": is_first_party, - "description": description, - "scopes": scopes, - "redirect_uris": redirect_uris, - "uses_pkce": uses_pkce, - "organization_id": organization_id, - } - - response = self._http_client.request( - CONNECT_APPLICATIONS_PATH, - method=REQUEST_METHOD_POST, - json=json, - ) - - return ConnectApplication.model_validate(response) - - def update_application( - self, - *, - application_id: str, - name: Optional[str] = None, - description: Optional[str] = None, - scopes: Optional[Sequence[str]] = None, - redirect_uris: Optional[Sequence[RedirectUriInput]] = None, - ) -> ConnectApplication: - json = { - "name": name, - "description": description, - "scopes": scopes, - "redirect_uris": redirect_uris, - } - - response = self._http_client.request( - f"{CONNECT_APPLICATIONS_PATH}/{application_id}", - method=REQUEST_METHOD_PUT, - json=json, - ) - - return ConnectApplication.model_validate(response) - - def delete_application(self, application_id: str) -> None: - self._http_client.request( - f"{CONNECT_APPLICATIONS_PATH}/{application_id}", - method=REQUEST_METHOD_DELETE, - ) - - def create_client_secret(self, application_id: str) -> ClientSecret: - response = self._http_client.request( - f"{CONNECT_APPLICATIONS_PATH}/{application_id}/client_secrets", - method=REQUEST_METHOD_POST, - json={}, - ) - - return ClientSecret.model_validate(response) - - def list_client_secrets( - self, - application_id: str, - ) -> Sequence[ClientSecret]: - response = self._http_client.request( - f"{CONNECT_APPLICATIONS_PATH}/{application_id}/client_secrets", - method=REQUEST_METHOD_GET, - ) - - return [ClientSecret.model_validate(secret) for secret in response] - - def delete_client_secret(self, client_secret_id: str) -> None: - self._http_client.request( - f"{CONNECT_CLIENT_SECRETS_PATH}/{client_secret_id}", - method=REQUEST_METHOD_DELETE, - ) - - -class AsyncConnect(ConnectModule): - _http_client: AsyncHTTPClient - - def __init__(self, http_client: AsyncHTTPClient): - self._http_client = http_client - - async def list_applications( - self, - *, - organization_id: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> ConnectApplicationsListResource: - list_params: ConnectApplicationListFilters = { - "organization_id": organization_id, - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = await self._http_client.request( - CONNECT_APPLICATIONS_PATH, - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return WorkOSListResource[ - ConnectApplication, ConnectApplicationListFilters, ListMetadata - ]( - list_method=self.list_applications, - list_args=list_params, - **ListPage[ConnectApplication](**response).model_dump(), - ) - - async def get_application(self, application_id: str) -> ConnectApplication: - response = await self._http_client.request( - f"{CONNECT_APPLICATIONS_PATH}/{application_id}", - method=REQUEST_METHOD_GET, - ) - - return ConnectApplication.model_validate(response) - - async def create_application( - self, - *, - name: str, - application_type: ApplicationType, - is_first_party: bool, - description: Optional[str] = None, - scopes: Optional[Sequence[str]] = None, - redirect_uris: Optional[Sequence[RedirectUriInput]] = None, - uses_pkce: Optional[bool] = None, - organization_id: Optional[str] = None, - ) -> ConnectApplication: - json = { - "name": name, - "application_type": application_type, - "is_first_party": is_first_party, - "description": description, - "scopes": scopes, - "redirect_uris": redirect_uris, - "uses_pkce": uses_pkce, - "organization_id": organization_id, - } - - response = await self._http_client.request( - CONNECT_APPLICATIONS_PATH, - method=REQUEST_METHOD_POST, - json=json, - ) - - return ConnectApplication.model_validate(response) - - async def update_application( - self, - *, - application_id: str, - name: Optional[str] = None, - description: Optional[str] = None, - scopes: Optional[Sequence[str]] = None, - redirect_uris: Optional[Sequence[RedirectUriInput]] = None, - ) -> ConnectApplication: - json = { - "name": name, - "description": description, - "scopes": scopes, - "redirect_uris": redirect_uris, - } - - response = await self._http_client.request( - f"{CONNECT_APPLICATIONS_PATH}/{application_id}", - method=REQUEST_METHOD_PUT, - json=json, - ) - - return ConnectApplication.model_validate(response) - - async def delete_application(self, application_id: str) -> None: - await self._http_client.request( - f"{CONNECT_APPLICATIONS_PATH}/{application_id}", - method=REQUEST_METHOD_DELETE, - ) - - async def create_client_secret(self, application_id: str) -> ClientSecret: - response = await self._http_client.request( - f"{CONNECT_APPLICATIONS_PATH}/{application_id}/client_secrets", - method=REQUEST_METHOD_POST, - json={}, - ) - - return ClientSecret.model_validate(response) - - async def list_client_secrets( - self, - application_id: str, - ) -> Sequence[ClientSecret]: - response = await self._http_client.request( - f"{CONNECT_APPLICATIONS_PATH}/{application_id}/client_secrets", - method=REQUEST_METHOD_GET, - ) - - return [ClientSecret.model_validate(secret) for secret in response] - - async def delete_client_secret(self, client_secret_id: str) -> None: - await self._http_client.request( - f"{CONNECT_CLIENT_SECRETS_PATH}/{client_secret_id}", - method=REQUEST_METHOD_DELETE, - ) diff --git a/src/workos/directory_sync.py b/src/workos/directory_sync.py deleted file mode 100644 index ded27fde..00000000 --- a/src/workos/directory_sync.py +++ /dev/null @@ -1,452 +0,0 @@ -from typing import Any, Dict, Optional, Protocol, Union - -from workos.types.directory_sync.list_filters import ( - DirectoryGroupListFilters, - DirectoryListFilters, - DirectoryUserListFilters, -) -from workos.typing.sync_or_async import SyncOrAsync -from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient -from workos.utils.pagination_order import PaginationOrder -from workos.utils.request_helper import ( - DEFAULT_LIST_RESPONSE_LIMIT, - REQUEST_METHOD_DELETE, - REQUEST_METHOD_GET, -) -from workos.types.directory_sync import ( - DirectoryGroup, - Directory, - DirectoryUserWithGroups, -) -from workos.types.list_resource import ListMetadata, ListPage, WorkOSListResource - -DirectoryUsersListResource = WorkOSListResource[ - DirectoryUserWithGroups, DirectoryUserListFilters, ListMetadata -] - -DirectoryGroupsListResource = WorkOSListResource[ - DirectoryGroup, DirectoryGroupListFilters, ListMetadata -] - -DirectoriesListResource = WorkOSListResource[ - Directory, DirectoryListFilters, ListMetadata -] - -# Mapping from SDK parameter names to API parameter names -PARAM_KEY_MAPPING = { - "directory_id": "directory", - "group_id": "group", - "user_id": "user", -} - - -def _prepare_request_params( - list_params: Union[DirectoryUserListFilters, DirectoryGroupListFilters], -) -> Dict[str, Any]: - """Convert list_params to API request params by renaming keys.""" - request_params: Dict[str, Any] = dict(list_params) - for sdk_key, api_key in PARAM_KEY_MAPPING.items(): - if sdk_key in request_params: - request_params[api_key] = request_params.pop(sdk_key) - return request_params - - -class DirectorySyncModule(Protocol): - """Offers methods through the WorkOS Directory Sync service.""" - - def list_users( - self, - *, - directory_id: Optional[str] = None, - group_id: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[DirectoryUsersListResource]: - """Gets a list of provisioned Users for a Directory. - - Note, either 'directory_id' or 'group_id' must be provided. - - Kwargs: - directory_id (str): Directory unique identifier. (Optional) - group_id (str): Directory Group unique identifier. (Optional) - limit (int): Maximum number of records to return. (Optional) - before (str): Pagination cursor to receive records before a provided Directory ID. (Optional) - after (str): Pagination cursor to receive records after a provided Directory ID. (Optional) - order (Literal["asc","desc"]): Sort records in either ascending or descending (default) order by created_at timestamp. (Optional) - - Returns: - DirectoryUsersListResource: Directory Users response from WorkOS. - """ - ... - - def list_groups( - self, - *, - directory_id: Optional[str] = None, - user_id: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[DirectoryGroupsListResource]: - """Gets a list of provisioned Groups for a Directory . - - Note, either 'directory_id' or 'user_id' must be provided. - - Kwargs: - directory_id (str): Directory unique identifier. (Optional) - user_id (str): Directory User unique identifier. (Optional) - limit (int): Maximum number of records to return. (Optional) - before (str): Pagination cursor to receive records before a provided Directory ID. (Optional) - after (str): Pagination cursor to receive records after a provided Directory ID. (Optional) - order (Literal["asc","desc"]): Sort records in either ascending or descending (default) order by created_at timestamp. (Optional) - - Returns: - DirectoryGroupsListResource: Directory Groups response from WorkOS. - """ - ... - - def list_directories( - self, - *, - search: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - organization_id: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[DirectoriesListResource]: - """Gets details for existing Directories. - - Kwargs: - organization_id: ID of an Organization (Optional) - search (str): Searchable text for a Directory. (Optional) - limit (int): Maximum number of records to return. (Optional) - before (str): Pagination cursor to receive records before a provided Directory ID. (Optional) - after (str): Pagination cursor to receive records after a provided Directory ID. (Optional) - order (Literal["asc","desc"]): Sort records in either ascending or descending (default) order by created_at timestamp. (Optional) - - Returns: - DirectoriesListResource: Directories response from WorkOS. - """ - ... - - def get_user(self, user_id: str) -> SyncOrAsync[DirectoryUserWithGroups]: - """Gets details for a single provisioned Directory User. - - Args: - user_id (str): Directory User unique identifier. - - Returns: - DirectoryUserWithGroups: Directory User response from WorkOS. - """ - ... - - def get_group(self, group_id: str) -> SyncOrAsync[DirectoryGroup]: - """Gets details for a single provisioned Directory Group. - - Args: - group_id (str): Directory Group unique identifier. - - Returns: - DirectoryGroup: Directory Group response from WorkOS. - """ - ... - - def get_directory(self, directory_id: str) -> SyncOrAsync[Directory]: - """Gets details for a single Directory - - Args: - directory_id (str): Directory unique identifier. - - Returns: - Directory: Directory response from WorkOS - """ - ... - - def delete_directory(self, directory_id: str) -> SyncOrAsync[None]: - """Delete one existing Directory. - - Args: - directory_id (str): Directory unique identifier. - - Returns: - None - """ - ... - - -class DirectorySync(DirectorySyncModule): - _http_client: SyncHTTPClient - - def __init__(self, http_client: SyncHTTPClient) -> None: - self._http_client = http_client - - def list_users( - self, - *, - directory_id: Optional[str] = None, - group_id: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> DirectoryUsersListResource: - list_params: DirectoryUserListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - if group_id is not None: - list_params["group_id"] = group_id - if directory_id is not None: - list_params["directory_id"] = directory_id - - response = self._http_client.request( - "directory_users", - method=REQUEST_METHOD_GET, - params=_prepare_request_params(list_params), - ) - - return WorkOSListResource( - list_method=self.list_users, - list_args=list_params, - **ListPage[DirectoryUserWithGroups](**response).model_dump(), - ) - - def list_groups( - self, - *, - directory_id: Optional[str] = None, - user_id: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> DirectoryGroupsListResource: - list_params: DirectoryGroupListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - if user_id is not None: - list_params["user_id"] = user_id - if directory_id is not None: - list_params["directory_id"] = directory_id - - response = self._http_client.request( - "directory_groups", - method=REQUEST_METHOD_GET, - params=_prepare_request_params(list_params), - ) - - return WorkOSListResource[ - DirectoryGroup, DirectoryGroupListFilters, ListMetadata - ]( - list_method=self.list_groups, - list_args=list_params, - **ListPage[DirectoryGroup](**response).model_dump(), - ) - - def get_user(self, user_id: str) -> DirectoryUserWithGroups: - response = self._http_client.request( - "directory_users/{user}".format(user=user_id), - method=REQUEST_METHOD_GET, - ) - - return DirectoryUserWithGroups.model_validate(response) - - def get_group(self, group_id: str) -> DirectoryGroup: - response = self._http_client.request( - "directory_groups/{group}".format(group=group_id), - method=REQUEST_METHOD_GET, - ) - return DirectoryGroup.model_validate(response) - - def get_directory(self, directory_id: str) -> Directory: - response = self._http_client.request( - "directories/{directory}".format(directory=directory_id), - method=REQUEST_METHOD_GET, - ) - - return Directory.model_validate(response) - - def list_directories( - self, - *, - search: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - organization_id: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> DirectoriesListResource: - list_params: DirectoryListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - "organization_id": organization_id, - "search": search, - } - - response = self._http_client.request( - "directories", - method=REQUEST_METHOD_GET, - params=list_params, - ) - return WorkOSListResource[Directory, DirectoryListFilters, ListMetadata]( - list_method=self.list_directories, - list_args=list_params, - **ListPage[Directory](**response).model_dump(), - ) - - def delete_directory(self, directory_id: str) -> None: - self._http_client.request( - "directories/{directory}".format(directory=directory_id), - method=REQUEST_METHOD_DELETE, - ) - - -class AsyncDirectorySync(DirectorySyncModule): - """Offers methods through the WorkOS Directory Sync service.""" - - _http_client: AsyncHTTPClient - - def __init__(self, http_client: AsyncHTTPClient): - self._http_client = http_client - - async def list_users( - self, - *, - directory_id: Optional[str] = None, - group_id: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> DirectoryUsersListResource: - list_params: DirectoryUserListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - if group_id is not None: - list_params["group_id"] = group_id - if directory_id is not None: - list_params["directory_id"] = directory_id - - response = await self._http_client.request( - "directory_users", - method=REQUEST_METHOD_GET, - params=_prepare_request_params(list_params), - ) - - return WorkOSListResource( - list_method=self.list_users, - list_args=list_params, - **ListPage[DirectoryUserWithGroups](**response).model_dump(), - ) - - async def list_groups( - self, - *, - directory_id: Optional[str] = None, - user_id: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> DirectoryGroupsListResource: - list_params: DirectoryGroupListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - if user_id is not None: - list_params["user_id"] = user_id - if directory_id is not None: - list_params["directory_id"] = directory_id - - response = await self._http_client.request( - "directory_groups", - method=REQUEST_METHOD_GET, - params=_prepare_request_params(list_params), - ) - - return WorkOSListResource[ - DirectoryGroup, DirectoryGroupListFilters, ListMetadata - ]( - list_method=self.list_groups, - list_args=list_params, - **ListPage[DirectoryGroup](**response).model_dump(), - ) - - async def get_user(self, user_id: str) -> DirectoryUserWithGroups: - response = await self._http_client.request( - "directory_users/{user}".format(user=user_id), - method=REQUEST_METHOD_GET, - ) - - return DirectoryUserWithGroups.model_validate(response) - - async def get_group(self, group_id: str) -> DirectoryGroup: - response = await self._http_client.request( - "directory_groups/{group}".format(group=group_id), - method=REQUEST_METHOD_GET, - ) - return DirectoryGroup.model_validate(response) - - async def get_directory(self, directory_id: str) -> Directory: - response = await self._http_client.request( - "directories/{directory}".format(directory=directory_id), - method=REQUEST_METHOD_GET, - ) - - return Directory.model_validate(response) - - async def list_directories( - self, - *, - search: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - organization_id: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> DirectoriesListResource: - list_params: DirectoryListFilters = { - "organization_id": organization_id, - "search": search, - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = await self._http_client.request( - "directories", - method=REQUEST_METHOD_GET, - params=list_params, - ) - return WorkOSListResource[Directory, DirectoryListFilters, ListMetadata]( - list_method=self.list_directories, - list_args=list_params, - **ListPage[Directory](**response).model_dump(), - ) - - async def delete_directory(self, directory_id: str) -> None: - await self._http_client.request( - "directories/{directory}".format(directory=directory_id), - method=REQUEST_METHOD_DELETE, - ) diff --git a/src/workos/events.py b/src/workos/events.py deleted file mode 100644 index e1c9e710..00000000 --- a/src/workos/events.py +++ /dev/null @@ -1,111 +0,0 @@ -from typing import Optional, Protocol, Sequence - -from workos.types.events.list_filters import EventsListFilters -from workos.typing.sync_or_async import SyncOrAsync -from workos.utils.request_helper import DEFAULT_LIST_RESPONSE_LIMIT, REQUEST_METHOD_GET -from workos.types.events import Event, EventType -from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient -from workos.types.list_resource import ListAfterMetadata, ListPage, WorkOSListResource - - -EventsListResource = WorkOSListResource[Event, EventsListFilters, ListAfterMetadata] - - -class EventsModule(Protocol): - """Offers methods through the WorkOS Events service.""" - - def list_events( - self, - *, - events: Sequence[EventType], - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - organization_id: Optional[str] = None, - after: Optional[str] = None, - range_start: Optional[str] = None, - range_end: Optional[str] = None, - ) -> SyncOrAsync[EventsListResource]: - """Gets a list of Events. - - Kwargs: - events (Sequence[EventType]): Filter to only return events of particular types. - limit (int): Maximum number of records to return. (Optional) - organization_id (str): Organization ID limits scope of events to a single organization. (Optional) - after (str): Pagination cursor to receive records after a provided Event ID. (Optional) - range_start (str): Date range start for stream of events. (Optional) - range_end (str): Date range end for stream of events. (Optional) - - Returns: - EventsListResource: Events response from WorkOS. - """ - ... - - -class Events(EventsModule): - _http_client: SyncHTTPClient - - def __init__(self, http_client: SyncHTTPClient): - self._http_client = http_client - - def list_events( - self, - *, - events: Sequence[EventType], - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - organization_id: Optional[str] = None, - after: Optional[str] = None, - range_start: Optional[str] = None, - range_end: Optional[str] = None, - ) -> EventsListResource: - params: EventsListFilters = { - "events": events, - "limit": limit, - "after": after, - "organization_id": organization_id, - "range_start": range_start, - "range_end": range_end, - } - - response = self._http_client.request( - "events", method=REQUEST_METHOD_GET, params=params - ) - return WorkOSListResource[Event, EventsListFilters, ListAfterMetadata]( - list_method=self.list_events, - list_args=params, - **ListPage[Event](**response).model_dump(exclude_unset=True), - ) - - -class AsyncEvents(EventsModule): - _http_client: AsyncHTTPClient - - def __init__(self, http_client: AsyncHTTPClient): - self._http_client = http_client - - async def list_events( - self, - *, - events: Sequence[EventType], - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - organization_id: Optional[str] = None, - after: Optional[str] = None, - range_start: Optional[str] = None, - range_end: Optional[str] = None, - ) -> EventsListResource: - params: EventsListFilters = { - "events": events, - "limit": limit, - "after": after, - "organization_id": organization_id, - "range_start": range_start, - "range_end": range_end, - } - - response = await self._http_client.request( - "events", method=REQUEST_METHOD_GET, params=params - ) - - return WorkOSListResource[Event, EventsListFilters, ListAfterMetadata]( - list_method=self.list_events, - list_args=params, - **ListPage[Event](**response).model_dump(exclude_unset=True), - ) diff --git a/src/workos/exceptions.py b/src/workos/exceptions.py deleted file mode 100644 index a79e1159..00000000 --- a/src/workos/exceptions.py +++ /dev/null @@ -1,83 +0,0 @@ -from typing import Any, Mapping, Optional - -import httpx - - -# Request related exceptions -class BaseRequestException(Exception): - def __init__( - self, - response: httpx.Response, - response_json: Optional[Mapping[str, Any]], - ) -> None: - super(BaseRequestException, self).__init__(response_json) - - self.response = response - self.response_json = response_json - - self.message = self.extract_from_json("message", "No message") - self.error = self.extract_from_json("error", "Unknown") - self.errors = self.extract_from_json("errors", None) - self.code = self.extract_from_json("code", None) - self.error_description = self.extract_from_json("error_description", "Unknown") - - self.request_id = response.headers.get("X-Request-ID") - - def extract_from_json(self, key: str, alt: Optional[str] = None) -> Optional[str]: - if self.response_json is None: - return alt - - return self.response_json.get(key, alt) - - def __str__(self) -> str: - exception = "(message=%s" % self.message - exception += ", request_id=%s" % self.request_id - - if self.response_json is not None: - for key, value in self.response_json.items(): - if key != "message": - exception += ", %s=%s" % (key, value) - - return exception + ")" - - -class AuthorizationException(BaseRequestException): - pass - - -class EmailVerificationRequiredException(AuthorizationException): - """Raised when email verification is required before authentication. - - This exception includes an email_verification_id field that can be used - to retrieve the email verification object or resend the verification email. - """ - - def __init__( - self, - response: httpx.Response, - response_json: Optional[Mapping[str, Any]], - ) -> None: - super().__init__(response, response_json) - self.email_verification_id = self.extract_from_json( - "email_verification_id", None - ) - - -class AuthenticationException(BaseRequestException): - pass - - -class BadRequestException(BaseRequestException): - pass - - -class ConflictException(BaseRequestException): - pass - - -class NotFoundException(BaseRequestException): - pass - - -class ServerException(BaseRequestException): - pass diff --git a/src/workos/fga.py b/src/workos/fga.py deleted file mode 100644 index c2bc8a20..00000000 --- a/src/workos/fga.py +++ /dev/null @@ -1,654 +0,0 @@ -import json -from typing import Any, Mapping, Optional, Protocol, Sequence -from workos.types.fga import ( - CheckOperation, - CheckResponse, - AuthorizationResource, - AuthorizationResourceType, - Warrant, - WarrantCheckInput, - WarrantWrite, - WarrantWriteOperation, - WriteWarrantResponse, - WarrantQueryResult, - FGAWarning, -) -from workos.types.fga.list_filters import ( - AuthorizationResourceListFilters, - WarrantListFilters, - WarrantQueryListFilters, -) -from workos.types.list_resource import ( - ListArgs, - ListMetadata, - ListPage, - WorkOSListResource, -) -from workos.utils.http_client import SyncHTTPClient -from workos.utils.pagination_order import PaginationOrder -from workos.utils.request_helper import ( - REQUEST_METHOD_DELETE, - REQUEST_METHOD_GET, - REQUEST_METHOD_POST, - REQUEST_METHOD_PUT, - RequestHelper, -) - -DEFAULT_RESPONSE_LIMIT = 10 - -AuthorizationResourceListResource = WorkOSListResource[ - AuthorizationResource, AuthorizationResourceListFilters, ListMetadata -] - -AuthorizationResourceTypeListResource = WorkOSListResource[ - AuthorizationResourceType, ListArgs, ListMetadata -] - -WarrantListResource = WorkOSListResource[Warrant, WarrantListFilters, ListMetadata] - - -class WarrantQueryListResource( - WorkOSListResource[WarrantQueryResult, WarrantQueryListFilters, ListMetadata] -): - warnings: Optional[Sequence[FGAWarning]] = None - - -class FGAModule(Protocol): - """ - .. deprecated:: - Use :class:`workos.authorization.AuthorizationModule` instead. - """ - - def get_resource( - self, *, resource_type: str, resource_id: str - ) -> AuthorizationResource: - """ - Get a warrant resource by its type and ID. - - Kwargs: - resource_type (str): The type of the resource. - resource_id (str): A unique identifier for the resource. - Returns: - Resource: A resource object. - """ - ... - - def list_resources( - self, - *, - resource_type: Optional[str] = None, - search: Optional[str] = None, - limit: int = DEFAULT_RESPONSE_LIMIT, - order: PaginationOrder = "desc", - before: Optional[str] = None, - after: Optional[str] = None, - ) -> AuthorizationResourceListResource: - """ - Gets a list of FGA resources. - - Kwargs: - resource_type (str): The type of the resource. (Optional) - search (str): Searchable text for a Resource. (Optional) - limit (int): The maximum number of resources to return. (Optional) - order (Literal["asc","desc"]): Sort warrant resources in either ascending or descending (default) order. (Optional) - before (str): A cursor to return resources before. (Optional) - after (str): A cursor to return resources after. (Optional) - Returns: - AuthorizationResourceListResource: A list of resources with built-in pagination iterator. - """ - ... - - def create_resource( - self, - *, - resource_type: str, - resource_id: str, - meta: Optional[Mapping[str, Any]] = None, - ) -> AuthorizationResource: - """ - Create a new warrant resource. - - Kwargs: - resource_type (str): The type of the resource. - resource_id (str): A unique identifier for the resource. - meta (Mapping): A dictionary containing additional information about this resource. (Optional) - Returns: - AuthorizationResource: A resource object. - """ - ... - - def update_resource( - self, - *, - resource_type: str, - resource_id: str, - meta: Optional[Mapping[str, Any]] = None, - ) -> AuthorizationResource: - """ - Updates an existing warrant resource. - - Kwargs: - resource_type (str): The type of the resource. - resource_id (str): A unique identifier for the resource. - meta (Mapping): A dictionary containing additional information about this resource. (Optional) - Returns: - AuthorizationResource: A resource object. - """ - ... - - def delete_resource(self, *, resource_type: str, resource_id: str) -> None: - """ - Deletes a resource by its type and ID. - - Kwargs: - resource_type (str): The type of the resource. - resource_id (str): A unique identifier for the resource. - - Returns: - None - """ - ... - - def list_resource_types( - self, - *, - limit: int = DEFAULT_RESPONSE_LIMIT, - order: PaginationOrder = "desc", - before: Optional[str] = None, - after: Optional[str] = None, - ) -> AuthorizationResourceTypeListResource: - """ - Gets a list of FGA resource types. - - Kwargs: - limit (int): The maximum number of resources to return. (Optional) - order (Literal["asc","desc"]): Sort warrant resource types in either ascending or descending (default) order. (Optional) - before (str): A cursor to return resources before. (Optional) - after (str): A cursor to return resources after. (Optional) - - Returns: - AuthorizationResourceTypeListResource: A list of resource types with built-in pagination iterator. - """ - ... - - def list_warrants( - self, - *, - subject_type: Optional[str] = None, - subject_id: Optional[str] = None, - subject_relation: Optional[str] = None, - relation: Optional[str] = None, - resource_type: Optional[str] = None, - resource_id: Optional[str] = None, - limit: int = DEFAULT_RESPONSE_LIMIT, - order: PaginationOrder = "desc", - before: Optional[str] = None, - after: Optional[str] = None, - warrant_token: Optional[str] = None, - ) -> WarrantListResource: - """ - Gets a list of warrants. - - Kwargs: - subject_type (str): The type of the subject. (Optional) - subject_id (str): The ID of the subject. (Optional) - subject_relation (str): The relation of the subject. (Optional) - relation (str): The relation of the warrant. (Optional) - resource_type (str): The type of the resource. (Optional) - resource_id (str): The ID of the resource. (Optional) - limit (int): The maximum number of resources to return. (Optional) - order (Literal["asc","desc"]): Sort warrants in either ascending or descending (default) order. (Optional) - before (str): A cursor to return resources before. (Optional) - after (str): A cursor to return resources after. (Optional) - warrant_token (str): The warrant token. (Optional) - - Returns: - WarrantListResource: A list of warrants with built-in pagination iterator. - """ - ... - - def write_warrant( - self, - *, - op: WarrantWriteOperation, - subject_type: str, - subject_id: str, - subject_relation: Optional[str] = None, - relation: str, - resource_type: str, - resource_id: str, - policy: Optional[str] = None, - ) -> WriteWarrantResponse: - """ - Write a warrant. - - Kwargs: - op (WarrantWriteOperation): The operation to perform. - subject_type (str): The type of the subject. - subject_id (str): The ID of the subject. - subject_relation (str): The relation of the subject. (Optional) - relation (str): The relation of the warrant. - resource_type (str): The type of the resource. - resource_id (str): The ID of the resource. - policy (str): The policy to apply. (Optional) - - Returns: - WriteWarrantResponse: The warrant token. - """ - ... - - def batch_write_warrants( - self, *, batch: Sequence[WarrantWrite] - ) -> WriteWarrantResponse: - """ - Write a batch of warrants. - - Args: - batch (Sequence[WarrantWrite]): A list of WarrantWrite objects. - - Returns: - WriteWarrantResponse: The warrant token. - """ - ... - - def check( - self, - *, - checks: Sequence[WarrantCheckInput], - op: Optional[CheckOperation] = None, - debug: bool = False, - warrant_token: Optional[str] = None, - ) -> CheckResponse: - """ - Check a warrant. - - Kwargs: - checks (Sequence[WarrantCheck]): A list of WarrantCheck objects. - op (CheckOperation): The operation to perform. (Optional) - debug (bool): Whether to return debug information including a decision tree. (Optional) - warrant_token (str): Optional token to specify desired read consistency. (Optional) - Returns: - CheckResponse: A check response. - """ - ... - - def check_batch( - self, - *, - checks: Sequence[WarrantCheckInput], - debug: bool = False, - warrant_token: Optional[str] = None, - ) -> Sequence[CheckResponse]: - """ - Check a batch of warrants. - - Kwargs: - checks (Sequence[WarrantCheck]): A list of WarrantCheck objects. - debug (bool): Whether to return debug information including a decision tree. (Optional) - warrant_token (str): Optional token to specify desired read consistency. (Optional) - Returns: - Sequence[CheckResponse]: A list of check responses - """ - ... - - def query( - self, - *, - q: str, - limit: int = DEFAULT_RESPONSE_LIMIT, - order: PaginationOrder = "desc", - before: Optional[str] = None, - after: Optional[str] = None, - context: Optional[Mapping[str, Any]] = None, - warrant_token: Optional[str] = None, - ) -> WarrantQueryListResource: - """ - Query for warrants. - - Kwargs: - q (str): The query string. - order (Literal["asc","desc"]): Sort warrant resources in either ascending or descending (default) order. (Optional) - order (str): The order in which to return resources. - before (str): A cursor to return resources before. (Optional) - after (str): A cursor to return resources after. (Optional) - context (Mapping): A dictionary containing additional context. (Optional) - warrant_token (str): Optional token to specify desired read consistency. (Optional) - Returns: - - QueryListResource: A list of query results with built-in pagination iterator. - """ - ... - - -class FGA(FGAModule): - _http_client: SyncHTTPClient - - def __init__(self, http_client: SyncHTTPClient): - self._http_client = http_client - - def get_resource( - self, - *, - resource_type: str, - resource_id: str, - ) -> AuthorizationResource: - if not resource_type or not resource_id: - raise ValueError( - "Incomplete arguments: 'resource_type' and 'resource_id' are required arguments" - ) - - response = self._http_client.request( - RequestHelper.build_parameterized_url( - "fga/v1/resources/{resource_type}/{resource_id}", - resource_type=resource_type, - resource_id=resource_id, - ), - method=REQUEST_METHOD_GET, - ) - - return AuthorizationResource.model_validate(response) - - def list_resources( - self, - *, - resource_type: Optional[str] = None, - search: Optional[str] = None, - limit: int = DEFAULT_RESPONSE_LIMIT, - order: PaginationOrder = "desc", - before: Optional[str] = None, - after: Optional[str] = None, - ) -> AuthorizationResourceListResource: - list_params: AuthorizationResourceListFilters = { - "resource_type": resource_type, - "search": search, - "limit": limit, - "order": order, - "before": before, - "after": after, - } - - response = self._http_client.request( - "fga/v1/resources", - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return AuthorizationResourceListResource( - list_method=self.list_resources, - list_args=list_params, - **ListPage[AuthorizationResource](**response).model_dump(), - ) - - def create_resource( - self, - *, - resource_type: str, - resource_id: str, - meta: Optional[Mapping[str, Any]] = None, - ) -> AuthorizationResource: - if not resource_type or not resource_id: - raise ValueError( - "Incomplete arguments: 'resource_type' and 'resource_id' are required arguments" - ) - - response = self._http_client.request( - "fga/v1/resources", - method=REQUEST_METHOD_POST, - json={ - "resource_type": resource_type, - "resource_id": resource_id, - "meta": meta, - }, - ) - - return AuthorizationResource.model_validate(response) - - def update_resource( - self, - *, - resource_type: str, - resource_id: str, - meta: Optional[Mapping[str, Any]] = None, - ) -> AuthorizationResource: - if not resource_type or not resource_id: - raise ValueError( - "Incomplete arguments: 'resource_type' and 'resource_id' are required arguments" - ) - - response = self._http_client.request( - RequestHelper.build_parameterized_url( - "fga/v1/resources/{resource_type}/{resource_id}", - resource_type=resource_type, - resource_id=resource_id, - ), - method=REQUEST_METHOD_PUT, - json={"meta": meta}, - ) - - return AuthorizationResource.model_validate(response) - - def delete_resource(self, *, resource_type: str, resource_id: str) -> None: - if not resource_type or not resource_id: - raise ValueError( - "Incomplete arguments: 'resource_type' and 'resource_id' are required arguments" - ) - - self._http_client.request( - RequestHelper.build_parameterized_url( - "fga/v1/resources/{resource_type}/{resource_id}", - resource_type=resource_type, - resource_id=resource_id, - ), - method=REQUEST_METHOD_DELETE, - ) - - def list_resource_types( - self, - *, - limit: int = DEFAULT_RESPONSE_LIMIT, - order: PaginationOrder = "desc", - before: Optional[str] = None, - after: Optional[str] = None, - ) -> AuthorizationResourceTypeListResource: - list_params: ListArgs = { - "limit": limit, - "order": order, - "before": before, - "after": after, - } - - response = self._http_client.request( - "fga/v1/resource-types", - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return AuthorizationResourceTypeListResource( - list_method=self.list_resource_types, - list_args=list_params, - **ListPage[AuthorizationResourceType](**response).model_dump(), - ) - - def list_warrants( - self, - *, - subject_type: Optional[str] = None, - subject_id: Optional[str] = None, - subject_relation: Optional[str] = None, - relation: Optional[str] = None, - resource_type: Optional[str] = None, - resource_id: Optional[str] = None, - limit: int = DEFAULT_RESPONSE_LIMIT, - order: PaginationOrder = "desc", - before: Optional[str] = None, - after: Optional[str] = None, - warrant_token: Optional[str] = None, - ) -> WarrantListResource: - list_params: WarrantListFilters = { - "resource_type": resource_type, - "resource_id": resource_id, - "relation": relation, - "subject_type": subject_type, - "subject_id": subject_id, - "subject_relation": subject_relation, - "limit": limit, - "order": order, - "before": before, - "after": after, - } - - response = self._http_client.request( - "fga/v1/warrants", - method=REQUEST_METHOD_GET, - params=list_params, - headers={"Warrant-Token": warrant_token} if warrant_token else None, - ) - - # A workaround to add warrant_token to the list_args for the ListResource iterator - list_params["warrant_token"] = warrant_token - - return WarrantListResource( - list_method=self.list_warrants, - list_args=list_params, - **ListPage[Warrant](**response).model_dump(), - ) - - def write_warrant( - self, - *, - op: WarrantWriteOperation, - subject_type: str, - subject_id: str, - subject_relation: Optional[str] = None, - relation: str, - resource_type: str, - resource_id: str, - policy: Optional[str] = None, - ) -> WriteWarrantResponse: - params = { - "op": op, - "resource_type": resource_type, - "resource_id": resource_id, - "relation": relation, - "subject": { - "resource_type": subject_type, - "resource_id": subject_id, - "relation": subject_relation, - }, - "policy": policy, - } - - response = self._http_client.request( - "fga/v1/warrants", - method=REQUEST_METHOD_POST, - json=params, - ) - - return WriteWarrantResponse.model_validate(response) - - def batch_write_warrants( - self, *, batch: Sequence[WarrantWrite] - ) -> WriteWarrantResponse: - if not batch: - raise ValueError("Incomplete arguments: No batch warrant writes provided") - - response = self._http_client.request( - "fga/v1/warrants", - method=REQUEST_METHOD_POST, - json=batch, - ) - - return WriteWarrantResponse.model_validate(response) - - def check( - self, - *, - checks: Sequence[WarrantCheckInput], - op: Optional[CheckOperation] = None, - debug: bool = False, - warrant_token: Optional[str] = None, - ) -> CheckResponse: - if not checks: - raise ValueError("Incomplete arguments: No checks provided") - - body = { - "checks": checks, - "op": op, - "debug": debug, - } - - response = self._http_client.request( - "fga/v1/check", - method=REQUEST_METHOD_POST, - json=body, - headers={"Warrant-Token": warrant_token} if warrant_token else None, - ) - - return CheckResponse.model_validate(response) - - def check_batch( - self, - *, - checks: Sequence[WarrantCheckInput], - debug: bool = False, - warrant_token: Optional[str] = None, - ) -> Sequence[CheckResponse]: - if not checks: - raise ValueError("Incomplete arguments: No checks provided") - - body = { - "checks": checks, - "op": "batch", - "debug": debug, - } - - response = self._http_client.request( - "fga/v1/check", - method=REQUEST_METHOD_POST, - json=body, - headers={"Warrant-Token": warrant_token} if warrant_token else None, - ) - - return [CheckResponse.model_validate(check) for check in response] - - def query( - self, - *, - q: str, - limit: int = DEFAULT_RESPONSE_LIMIT, - order: PaginationOrder = "desc", - before: Optional[str] = None, - after: Optional[str] = None, - context: Optional[Mapping[str, Any]] = None, - warrant_token: Optional[str] = None, - ) -> WarrantQueryListResource: - list_params: WarrantQueryListFilters = { - "q": q, - "limit": limit, - "order": order, - "before": before, - "after": after, - "context": context, - } - parsed_list_params = { - key: json.dumps(value) if key == "context" and value is not None else value - for key, value in list_params.items() - if value is not None - } - - response = self._http_client.request( - "fga/v1/query", - method=REQUEST_METHOD_GET, - params=parsed_list_params, - headers={"Warrant-Token": warrant_token} if warrant_token else None, - ) - - # A workaround to add warrant_token to the list_args for the ListResource iterator - list_params["warrant_token"] = warrant_token - - return WarrantQueryListResource( - list_method=self.query, - list_args=list_params, - warnings=response.get("warnings"), - **ListPage[WarrantQueryResult](**response).model_dump(), - ) diff --git a/src/workos/mfa.py b/src/workos/mfa.py deleted file mode 100644 index 3cf62018..00000000 --- a/src/workos/mfa.py +++ /dev/null @@ -1,205 +0,0 @@ -from typing import Optional, Protocol - -from workos.types.mfa.enroll_authentication_factor_type import ( - EnrollAuthenticationFactorType, -) -from workos.utils.http_client import SyncHTTPClient -from workos.utils.request_helper import ( - REQUEST_METHOD_POST, - REQUEST_METHOD_DELETE, - REQUEST_METHOD_GET, - RequestHelper, -) -from workos.types.mfa import ( - AuthenticationChallenge, - AuthenticationChallengeVerificationResponse, - AuthenticationFactor, - AuthenticationFactorExtended, - AuthenticationFactorSms, - AuthenticationFactorTotp, - AuthenticationFactorTotpExtended, -) - - -class MFAModule(Protocol): - """Offers methods through the WorkOS MFA service.""" - - def enroll_factor( - self, - *, - type: EnrollAuthenticationFactorType, - totp_issuer: Optional[str] = None, - totp_user: Optional[str] = None, - phone_number: Optional[str] = None, - ) -> AuthenticationFactorExtended: - """ - Defines the type of MFA authorization factor to be used. Possible values are sms or totp. - - Kwargs: - type (str): The type of factor to be enrolled (sms or totp). - totp_issuer (str): Name of the Organization. Required when type is totp, ignored otherwise. - totp_user (str): email of user. Required when type is totp, ignored otherwise. - phone_number (str): phone number of the user. (Optional) - - Returns: - AuthenticationFactor: - """ - ... - - def get_factor(self, authentication_factor_id: str) -> AuthenticationFactor: - """ - Returns an authorization factor from its ID. - - Args: - authentication_factor_id (str): The ID of the factor to be obtained. - - Returns: - AuthenticationFactor: AuthenticationFactor response from WorkOS. - """ - ... - - def delete_factor(self, authentication_factor_id: str) -> None: - """ - Deletes an MFA authorization factor. - - Args: - authentication_factor_id (str): The ID of the authorization factor to be deleted. - - Returns: - None - """ - ... - - def challenge_factor( - self, *, authentication_factor_id: str, sms_template: Optional[str] = None - ) -> AuthenticationChallenge: - """ - Initiates the authentication process for the newly created MFA authorization factor, referred to as a challenge. - - Kwargs: - authentication_factor_id (str): ID of the authorization factor - sms_template (str): Optional parameter to customize the message for sms type factors. Must include "{{code}}" if used. (Optional) - - Returns: - AuthenticationChallenge: AuthenticationChallenge response from WorkOS. - """ - ... - - def verify_challenge( - self, *, authentication_challenge_id: str, code: str - ) -> AuthenticationChallengeVerificationResponse: - """ - Verifies the one time password provided by the end-user. - - Kwargs: - authentication_challenge_id (str): The ID of the authentication challenge that provided the user the verification code. - code (str): The verification code sent to and provided by the end user. - - Returns: - AuthenticationChallengeVerificationResponse: AuthenticationChallengeVerificationResponse response from WorkOS. - """ - ... - - -class Mfa(MFAModule): - """Methods to assist in creating, challenging, and verifying Authentication Factors through the WorkOS MFA service.""" - - _http_client: SyncHTTPClient - - def __init__(self, http_client: SyncHTTPClient): - self._http_client = http_client - - def enroll_factor( - self, - *, - type: EnrollAuthenticationFactorType, - totp_issuer: Optional[str] = None, - totp_user: Optional[str] = None, - phone_number: Optional[str] = None, - ) -> AuthenticationFactorExtended: - json = { - "type": type, - "totp_issuer": totp_issuer, - "totp_user": totp_user, - "phone_number": phone_number, - } - - if type == "totp" and (totp_issuer is None or totp_user is None): - raise ValueError( - "Incomplete arguments. Need to specify both totp_issuer and totp_user when type is totp" - ) - - if type == "sms" and phone_number is None: - raise ValueError( - "Incomplete arguments. Need to specify phone_number when type is sms" - ) - - response = self._http_client.request( - "auth/factors/enroll", method=REQUEST_METHOD_POST, json=json - ) - - if type == "totp": - return AuthenticationFactorTotpExtended.model_validate(response) - - return AuthenticationFactorSms.model_validate(response) - - def get_factor(self, authentication_factor_id: str) -> AuthenticationFactor: - response = self._http_client.request( - RequestHelper.build_parameterized_url( - "auth/factors/{authentication_factor_id}", - authentication_factor_id=authentication_factor_id, - ), - method=REQUEST_METHOD_GET, - ) - - if response["type"] == "totp": - return AuthenticationFactorTotp.model_validate(response) - - return AuthenticationFactorSms.model_validate(response) - - def delete_factor(self, authentication_factor_id: str) -> None: - self._http_client.request( - RequestHelper.build_parameterized_url( - "auth/factors/{authentication_factor_id}", - authentication_factor_id=authentication_factor_id, - ), - method=REQUEST_METHOD_DELETE, - ) - - def challenge_factor( - self, - *, - authentication_factor_id: str, - sms_template: Optional[str] = None, - ) -> AuthenticationChallenge: - json = { - "sms_template": sms_template, - } - - response = self._http_client.request( - RequestHelper.build_parameterized_url( - "auth/factors/{factor_id}/challenge", factor_id=authentication_factor_id - ), - method=REQUEST_METHOD_POST, - json=json, - ) - - return AuthenticationChallenge.model_validate(response) - - def verify_challenge( - self, *, authentication_challenge_id: str, code: str - ) -> AuthenticationChallengeVerificationResponse: - json = { - "code": code, - } - - response = self._http_client.request( - RequestHelper.build_parameterized_url( - "auth/challenges/{challenge_id}/verify", - challenge_id=authentication_challenge_id, - ), - method=REQUEST_METHOD_POST, - json=json, - ) - - return AuthenticationChallengeVerificationResponse.model_validate(response) diff --git a/src/workos/organization_domains.py b/src/workos/organization_domains.py deleted file mode 100644 index 173fa17a..00000000 --- a/src/workos/organization_domains.py +++ /dev/null @@ -1,179 +0,0 @@ -from typing import Protocol -from workos._client_configuration import ClientConfiguration -from workos.types.organization_domains import OrganizationDomain -from workos.typing.sync_or_async import SyncOrAsync -from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient -from workos.utils.request_helper import ( - REQUEST_METHOD_DELETE, - REQUEST_METHOD_GET, - REQUEST_METHOD_POST, -) - - -class OrganizationDomainsModule(Protocol): - """Offers methods for managing organization domains.""" - - _client_configuration: ClientConfiguration - - def get_organization_domain( - self, organization_domain_id: str - ) -> SyncOrAsync[OrganizationDomain]: - """Gets a single Organization Domain - - Args: - organization_domain_id (str): Organization Domain unique identifier - - Returns: - OrganizationDomain: Organization Domain response from WorkOS - """ - ... - - def create_organization_domain( - self, - organization_id: str, - domain: str, - ) -> SyncOrAsync[OrganizationDomain]: - """Creates an Organization Domain - - Args: - organization_id (str): Organization unique identifier - domain (str): Domain to be added to the organization - - Returns: - OrganizationDomain: Organization Domain response from WorkOS - """ - ... - - def verify_organization_domain( - self, organization_domain_id: str - ) -> SyncOrAsync[OrganizationDomain]: - """Verifies an Organization Domain - - Args: - organization_domain_id (str): Organization Domain unique identifier - - Returns: - OrganizationDomain: Organization Domain response from WorkOS - """ - ... - - def delete_organization_domain( - self, organization_domain_id: str - ) -> SyncOrAsync[None]: - """Deletes a single Organization Domain - - Args: - organization_domain_id (str): Organization Domain unique identifier - - Returns: - None - """ - ... - - -class OrganizationDomains: - """Offers methods for managing organization domains.""" - - _http_client: SyncHTTPClient - _client_configuration: ClientConfiguration - - def __init__( - self, - http_client: SyncHTTPClient, - client_configuration: ClientConfiguration, - ): - self._http_client = http_client - self._client_configuration = client_configuration - - def get_organization_domain( - self, organization_domain_id: str - ) -> OrganizationDomain: - response = self._http_client.request( - f"organization_domains/{organization_domain_id}", - method=REQUEST_METHOD_GET, - ) - - return OrganizationDomain.model_validate(response) - - def create_organization_domain( - self, - organization_id: str, - domain: str, - ) -> OrganizationDomain: - response = self._http_client.request( - "organization_domains", - method=REQUEST_METHOD_POST, - json={"organization_id": organization_id, "domain": domain}, - ) - - return OrganizationDomain.model_validate(response) - - def verify_organization_domain( - self, organization_domain_id: str - ) -> OrganizationDomain: - response = self._http_client.request( - f"organization_domains/{organization_domain_id}/verify", - method=REQUEST_METHOD_POST, - ) - - return OrganizationDomain.model_validate(response) - - def delete_organization_domain(self, organization_domain_id: str) -> None: - self._http_client.request( - f"organization_domains/{organization_domain_id}", - method=REQUEST_METHOD_DELETE, - ) - - -class AsyncOrganizationDomains: - """Offers async methods for managing organization domains.""" - - _http_client: AsyncHTTPClient - _client_configuration: ClientConfiguration - - def __init__( - self, - http_client: AsyncHTTPClient, - client_configuration: ClientConfiguration, - ): - self._http_client = http_client - self._client_configuration = client_configuration - - async def get_organization_domain( - self, organization_domain_id: str - ) -> OrganizationDomain: - response = await self._http_client.request( - f"organization_domains/{organization_domain_id}", - method=REQUEST_METHOD_GET, - ) - - return OrganizationDomain.model_validate(response) - - async def create_organization_domain( - self, - organization_id: str, - domain: str, - ) -> OrganizationDomain: - response = await self._http_client.request( - "organization_domains", - method=REQUEST_METHOD_POST, - json={"organization_id": organization_id, "domain": domain}, - ) - - return OrganizationDomain.model_validate(response) - - async def verify_organization_domain( - self, organization_domain_id: str - ) -> OrganizationDomain: - response = await self._http_client.request( - f"organization_domains/{organization_domain_id}/verify", - method=REQUEST_METHOD_POST, - ) - - return OrganizationDomain.model_validate(response) - - async def delete_organization_domain(self, organization_domain_id: str) -> None: - await self._http_client.request( - f"organization_domains/{organization_domain_id}", - method=REQUEST_METHOD_DELETE, - ) diff --git a/src/workos/organizations.py b/src/workos/organizations.py deleted file mode 100644 index 2cf6276b..00000000 --- a/src/workos/organizations.py +++ /dev/null @@ -1,602 +0,0 @@ -from functools import partial -from typing import Optional, Protocol, Sequence - -from workos.types.api_keys import ApiKey, ApiKeyWithValue -from workos.types.api_keys.list_filters import ApiKeyListFilters -from workos.types.feature_flags import FeatureFlag -from workos.types.feature_flags.list_filters import FeatureFlagListFilters -from workos.types.metadata import Metadata -from workos.types.organizations.domain_data_input import DomainDataInput -from workos.types.organizations.list_filters import OrganizationListFilters -from workos.types.roles.role import RoleList -from workos.typing.sync_or_async import SyncOrAsync -from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient -from workos.utils.pagination_order import PaginationOrder -from workos.utils.request_helper import ( - DEFAULT_LIST_RESPONSE_LIMIT, - REQUEST_METHOD_DELETE, - REQUEST_METHOD_GET, - REQUEST_METHOD_POST, - REQUEST_METHOD_PUT, -) -from workos.types.organizations import Organization -from workos.types.list_resource import ListMetadata, ListPage, WorkOSListResource - -ORGANIZATIONS_PATH = "organizations" - - -OrganizationsListResource = WorkOSListResource[ - Organization, OrganizationListFilters, ListMetadata -] - -FeatureFlagsListResource = WorkOSListResource[ - FeatureFlag, FeatureFlagListFilters, ListMetadata -] - -ApiKeysListResource = WorkOSListResource[ApiKey, ApiKeyListFilters, ListMetadata] - - -class OrganizationsModule(Protocol): - """Offers methods through the WorkOS Organizations service.""" - - def list_organizations( - self, - *, - domains: Optional[Sequence[str]] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[OrganizationsListResource]: - """Retrieve a list of organizations that have connections configured within your WorkOS dashboard. - - Kwargs: - domains (list): Filter organizations to only return those that are associated with the provided domains. (Optional) - limit (int): Maximum number of records to return. (Optional) - before (str): Pagination cursor to receive records before a provided Organization ID. (Optional) - after (str): Pagination cursor to receive records after a provided Organization ID. (Optional) - order (Literal["asc","desc"]): Sort records in either ascending or descending (default) order by created_at timestamp. (Optional) - - Returns: - OrganizationsListResource: Organizations list response from WorkOS. - """ - ... - - def get_organization(self, organization_id: str) -> SyncOrAsync[Organization]: - """Gets details for a single Organization - - Args: - organization_id (str): Organization's unique identifier - Returns: - Organization: Organization response from WorkOS - """ - ... - - def get_organization_by_external_id( - self, external_id: str - ) -> SyncOrAsync[Organization]: - """Gets details for a single Organization by external id - - Args: - external_id (str): Organization's external id - - Returns: - Organization: Organization response from WorkOS - """ - ... - - def create_organization( - self, - *, - name: str, - domain_data: Optional[Sequence[DomainDataInput]] = None, - idempotency_key: Optional[str] = None, - external_id: Optional[str] = None, - metadata: Optional[Metadata] = None, - ) -> SyncOrAsync[Organization]: - """Create an organization - - Kwargs: - name (str): A descriptive name for the organization. (Optional) - domain_data (Sequence[DomainDataInput]): List of domains that belong to the organization. (Optional) - idempotency_key (str): Key to guarantee idempotency across requests. (Optional) - - Returns: - Organization: Updated Organization response from WorkOS. - """ - ... - - def update_organization( - self, - *, - organization_id: str, - name: Optional[str] = None, - domain_data: Optional[Sequence[DomainDataInput]] = None, - external_id: Optional[str] = None, - metadata: Optional[Metadata] = None, - ) -> SyncOrAsync[Organization]: - """Update an organization - - Kwargs: - organization (str): Organization's unique identifier. - name (str): A descriptive name for the organization. (Optional) - domain_data (Sequence[DomainDataInput]): List of domains that belong to the organization. (Optional) - stripe_customer_id (str): The ID of the Stripe customer associated with the organization. (Optional) - - Returns: - Organization: Updated Organization response from WorkOS. - """ - ... - - def delete_organization(self, organization_id: str) -> SyncOrAsync[None]: - """Deletes a single Organization - - Args: - organization_id (str): Organization unique identifier - - Returns: - None - """ - ... - - def list_feature_flags( - self, - organization_id: str, - *, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[FeatureFlagsListResource]: - """Retrieve a list of feature flags for an organization - - Args: - organization_id (str): Organization's unique identifier - limit (int): Maximum number of records to return. (Optional) - before (str): Pagination cursor to receive records before a provided Feature Flag ID. (Optional) - after (str): Pagination cursor to receive records after a provided Feature Flag ID. (Optional) - order (Literal["asc","desc"]): Sort records in either ascending or descending (default) order by created_at timestamp. (Optional) - - Returns: - FeatureFlagsListResource: Feature flags list response from WorkOS. - """ - ... - - def create_api_key( - self, - organization_id: str, - *, - name: str, - permissions: Optional[Sequence[str]] = None, - ) -> SyncOrAsync[ApiKeyWithValue]: - """Create an API key for an organization. - - The response includes the full API key value which is only returned once - at creation time. Make sure to store this value securely. - - Args: - organization_id (str): Organization's unique identifier - - Kwargs: - name (str): A descriptive name for the API key - permissions (Sequence[str]): List of permissions to assign to the key (Optional) - - Returns: - ApiKeyWithValue: API key with the full value field - """ - ... - - def list_api_keys( - self, - organization_id: str, - *, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[ApiKeysListResource]: - """Retrieve a list of API keys for an organization - - Args: - organization_id (str): Organization's unique identifier - - Kwargs: - limit (int): Maximum number of records to return. (Optional) - before (str): Pagination cursor to receive records before a provided API Key ID. (Optional) - after (str): Pagination cursor to receive records after a provided API Key ID. (Optional) - order (Literal["asc","desc"]): Sort records in either ascending or descending (default) order by created_at timestamp. (Optional) - - Returns: - ApiKeysListResource: API keys list response from WorkOS. - """ - ... - - -class Organizations(OrganizationsModule): - _http_client: SyncHTTPClient - - def __init__(self, http_client: SyncHTTPClient): - self._http_client = http_client - - def list_organizations( - self, - *, - domains: Optional[Sequence[str]] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> OrganizationsListResource: - list_params: OrganizationListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - "domains": domains, - } - - response = self._http_client.request( - ORGANIZATIONS_PATH, - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return WorkOSListResource[Organization, OrganizationListFilters, ListMetadata]( - list_method=self.list_organizations, - list_args=list_params, - **ListPage[Organization](**response).model_dump(), - ) - - def get_organization(self, organization_id: str) -> Organization: - response = self._http_client.request( - f"organizations/{organization_id}", method=REQUEST_METHOD_GET - ) - - return Organization.model_validate(response) - - def get_organization_by_external_id(self, external_id: str) -> Organization: - response = self._http_client.request( - "organizations/external_id/{external_id}".format(external_id=external_id), - method=REQUEST_METHOD_GET, - ) - - return Organization.model_validate(response) - - def create_organization( - self, - *, - name: str, - domain_data: Optional[Sequence[DomainDataInput]] = None, - idempotency_key: Optional[str] = None, - external_id: Optional[str] = None, - metadata: Optional[Metadata] = None, - ) -> Organization: - headers = {} - if idempotency_key: - headers["idempotency-key"] = idempotency_key - - json = { - "name": name, - "domain_data": domain_data, - "idempotency_key": idempotency_key, - "external_id": external_id, - "metadata": metadata, - } - - response = self._http_client.request( - ORGANIZATIONS_PATH, - method=REQUEST_METHOD_POST, - json=json, - headers=headers, - ) - - return Organization.model_validate(response) - - def update_organization( - self, - *, - organization_id: str, - name: Optional[str] = None, - domain_data: Optional[Sequence[DomainDataInput]] = None, - stripe_customer_id: Optional[str] = None, - external_id: Optional[str] = None, - metadata: Optional[Metadata] = None, - ) -> Organization: - json = { - "name": name, - "domain_data": domain_data, - "stripe_customer_id": stripe_customer_id, - "external_id": external_id, - "metadata": metadata, - } - - response = self._http_client.request( - f"organizations/{organization_id}", method=REQUEST_METHOD_PUT, json=json - ) - - return Organization.model_validate(response) - - def delete_organization(self, organization_id: str) -> None: - self._http_client.request( - f"organizations/{organization_id}", - method=REQUEST_METHOD_DELETE, - ) - - def list_organization_roles(self, organization_id: str) -> RoleList: - response = self._http_client.request( - f"organizations/{organization_id}/roles", - method=REQUEST_METHOD_GET, - ) - - return RoleList.model_validate(response) - - def list_feature_flags( - self, - organization_id: str, - *, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> FeatureFlagsListResource: - list_params: FeatureFlagListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = self._http_client.request( - f"organizations/{organization_id}/feature-flags", - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return WorkOSListResource[FeatureFlag, FeatureFlagListFilters, ListMetadata]( - list_method=self.list_feature_flags, - list_args=list_params, - **ListPage[FeatureFlag](**response).model_dump(), - ) - - def create_api_key( - self, - organization_id: str, - *, - name: str, - permissions: Optional[Sequence[str]] = None, - ) -> ApiKeyWithValue: - json = { - "name": name, - "permissions": permissions, - } - - response = self._http_client.request( - f"organizations/{organization_id}/api_keys", - method=REQUEST_METHOD_POST, - json=json, - ) - - return ApiKeyWithValue.model_validate(response) - - def list_api_keys( - self, - organization_id: str, - *, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> ApiKeysListResource: - list_params: ApiKeyListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = self._http_client.request( - f"organizations/{organization_id}/api_keys", - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return WorkOSListResource[ApiKey, ApiKeyListFilters, ListMetadata]( - list_method=partial(self.list_api_keys, organization_id), - list_args=list_params, - **ListPage[ApiKey](**response).model_dump(), - ) - - -class AsyncOrganizations(OrganizationsModule): - _http_client: AsyncHTTPClient - - def __init__(self, http_client: AsyncHTTPClient): - self._http_client = http_client - - async def list_organizations( - self, - *, - domains: Optional[Sequence[str]] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> OrganizationsListResource: - list_params: OrganizationListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - "domains": domains, - } - - response = await self._http_client.request( - ORGANIZATIONS_PATH, - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return WorkOSListResource[Organization, OrganizationListFilters, ListMetadata]( - list_method=self.list_organizations, - list_args=list_params, - **ListPage[Organization](**response).model_dump(), - ) - - async def get_organization(self, organization_id: str) -> Organization: - response = await self._http_client.request( - f"organizations/{organization_id}", method=REQUEST_METHOD_GET - ) - - return Organization.model_validate(response) - - async def get_organization_by_external_id(self, external_id: str) -> Organization: - response = await self._http_client.request( - "organizations/external_id/{external_id}".format(external_id=external_id), - method=REQUEST_METHOD_GET, - ) - - return Organization.model_validate(response) - - async def create_organization( - self, - *, - name: str, - domain_data: Optional[Sequence[DomainDataInput]] = None, - idempotency_key: Optional[str] = None, - external_id: Optional[str] = None, - metadata: Optional[Metadata] = None, - ) -> Organization: - headers = {} - if idempotency_key: - headers["idempotency-key"] = idempotency_key - - json = { - "name": name, - "domain_data": domain_data, - "idempotency_key": idempotency_key, - "external_id": external_id, - "metadata": metadata, - } - - response = await self._http_client.request( - ORGANIZATIONS_PATH, - method=REQUEST_METHOD_POST, - json=json, - headers=headers, - ) - - return Organization.model_validate(response) - - async def update_organization( - self, - *, - organization_id: str, - name: Optional[str] = None, - domain_data: Optional[Sequence[DomainDataInput]] = None, - stripe_customer_id: Optional[str] = None, - external_id: Optional[str] = None, - metadata: Optional[Metadata] = None, - ) -> Organization: - json = { - "name": name, - "domain_data": domain_data, - "stripe_customer_id": stripe_customer_id, - "external_id": external_id, - "metadata": metadata, - } - - response = await self._http_client.request( - f"organizations/{organization_id}", method=REQUEST_METHOD_PUT, json=json - ) - - return Organization.model_validate(response) - - async def delete_organization(self, organization_id: str) -> None: - await self._http_client.request( - f"organizations/{organization_id}", - method=REQUEST_METHOD_DELETE, - ) - - async def list_organization_roles(self, organization_id: str) -> RoleList: - response = await self._http_client.request( - f"organizations/{organization_id}/roles", - method=REQUEST_METHOD_GET, - ) - - return RoleList.model_validate(response) - - async def list_feature_flags( - self, - organization_id: str, - *, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> FeatureFlagsListResource: - list_params: FeatureFlagListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = await self._http_client.request( - f"organizations/{organization_id}/feature-flags", - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return WorkOSListResource[FeatureFlag, FeatureFlagListFilters, ListMetadata]( - list_method=self.list_feature_flags, - list_args=list_params, - **ListPage[FeatureFlag](**response).model_dump(), - ) - - async def create_api_key( - self, - organization_id: str, - *, - name: str, - permissions: Optional[Sequence[str]] = None, - ) -> ApiKeyWithValue: - json = { - "name": name, - "permissions": permissions, - } - - response = await self._http_client.request( - f"organizations/{organization_id}/api_keys", - method=REQUEST_METHOD_POST, - json=json, - ) - - return ApiKeyWithValue.model_validate(response) - - async def list_api_keys( - self, - organization_id: str, - *, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> ApiKeysListResource: - list_params: ApiKeyListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = await self._http_client.request( - f"organizations/{organization_id}/api_keys", - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return WorkOSListResource[ApiKey, ApiKeyListFilters, ListMetadata]( - list_method=partial(self.list_api_keys, organization_id), - list_args=list_params, - **ListPage[ApiKey](**response).model_dump(), - ) diff --git a/src/workos/passwordless.py b/src/workos/passwordless.py deleted file mode 100644 index f3143551..00000000 --- a/src/workos/passwordless.py +++ /dev/null @@ -1,91 +0,0 @@ -from typing import Literal, Optional, Protocol - -from workos.types.passwordless.passwordless_session_type import PasswordlessSessionType -from workos.utils.http_client import SyncHTTPClient -from workos.utils.request_helper import REQUEST_METHOD_POST -from workos.types.passwordless.passwordless_session import PasswordlessSession - - -class PasswordlessModule(Protocol): - """Offers methods through the WorkOS Passwordless service.""" - - def create_session( - self, - *, - email: str, - type: PasswordlessSessionType, - redirect_uri: Optional[str] = None, - state: Optional[str] = None, - expires_in: Optional[int] = None, - ) -> PasswordlessSession: - """Create a Passwordless Session. - - Kwargs: - email (str): The email of the user to authenticate. - type (PasswordlessSessionType): The type of Passwordless Session to - create. Currently, the only supported value is 'MagicLink'. - redirect_uri (str): Optional parameter to - specify the redirect endpoint which will handle the callback - from WorkOS. Defaults to the default Redirect URI in the - WorkOS dashboard. (Optional) - state (str): Optional parameter that the redirect - URI received from WorkOS will contain. The state parameter - can be used to encode arbitrary information to help - restore application state between redirects. (Optional) - expires_in (int): The number of seconds the Passwordless Session should live before expiring. - This value must be between 900 (15 minutes) and 86400 (24 hours), inclusive. (Optional) - - Returns: - PasswordlessSession: A passwordless session object. - """ - ... - - def send_session(self, session_id: str) -> Literal[True]: - """Send a Passwordless Session via email. - - Args: - session_id (str): The unique identifier of the Passwordless - Session to send an email for. - - Returns: - boolean: Returns True - """ - ... - - -class Passwordless(PasswordlessModule): - _http_client: SyncHTTPClient - - def __init__(self, http_client: SyncHTTPClient): - self._http_client = http_client - - def create_session( - self, - *, - email: str, - type: PasswordlessSessionType, - redirect_uri: Optional[str] = None, - state: Optional[str] = None, - expires_in: Optional[int] = None, - ) -> PasswordlessSession: - json = { - "email": email, - "type": type, - "expires_in": expires_in, - "redirect_uri": redirect_uri, - "state": state, - } - - response = self._http_client.request( - "passwordless/sessions", method=REQUEST_METHOD_POST, json=json - ) - - return PasswordlessSession.model_validate(response) - - def send_session(self, session_id: str) -> Literal[True]: - self._http_client.request( - "passwordless/sessions/{session_id}/send".format(session_id=session_id), - method=REQUEST_METHOD_POST, - ) - - return True diff --git a/src/workos/pipes.py b/src/workos/pipes.py deleted file mode 100644 index 481417cf..00000000 --- a/src/workos/pipes.py +++ /dev/null @@ -1,93 +0,0 @@ -from typing import Dict, Optional, Protocol - -from workos.types.pipes import ( - GetAccessTokenFailureResponse, - GetAccessTokenResponse, - GetAccessTokenSuccessResponse, -) -from workos.typing.sync_or_async import SyncOrAsync -from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient -from workos.utils.request_helper import REQUEST_METHOD_POST - - -class PipesModule(Protocol): - """Protocol defining the Pipes module interface.""" - - def get_access_token( - self, - *, - provider: str, - user_id: str, - organization_id: Optional[str] = None, - ) -> SyncOrAsync[GetAccessTokenResponse]: - """Retrieve an access token for a third-party provider. - - Kwargs: - provider (str): The third-party provider identifier - user_id (str): The WorkOS user ID - organization_id (str, optional): The WorkOS organization ID - - Returns: - GetAccessTokenResponse: Success response with token or failure response with error - """ - ... - - -class Pipes(PipesModule): - """Sync implementation of the Pipes module.""" - - _http_client: SyncHTTPClient - - def __init__(self, http_client: SyncHTTPClient): - self._http_client = http_client - - def get_access_token( - self, - *, - provider: str, - user_id: str, - organization_id: Optional[str] = None, - ) -> GetAccessTokenResponse: - json_data: Dict[str, str] = {"user_id": user_id} - if organization_id is not None: - json_data["organization_id"] = organization_id - - response = self._http_client.request( - f"data-integrations/{provider}/token", - method=REQUEST_METHOD_POST, - json=json_data, - ) - - if response.get("active") is True: - return GetAccessTokenSuccessResponse.model_validate(response) - return GetAccessTokenFailureResponse.model_validate(response) - - -class AsyncPipes(PipesModule): - """Async implementation of the Pipes module.""" - - _http_client: AsyncHTTPClient - - def __init__(self, http_client: AsyncHTTPClient): - self._http_client = http_client - - async def get_access_token( - self, - *, - provider: str, - user_id: str, - organization_id: Optional[str] = None, - ) -> GetAccessTokenResponse: - json_data: Dict[str, str] = {"user_id": user_id} - if organization_id is not None: - json_data["organization_id"] = organization_id - - response = await self._http_client.request( - f"data-integrations/{provider}/token", - method=REQUEST_METHOD_POST, - json=json_data, - ) - - if response.get("active") is True: - return GetAccessTokenSuccessResponse.model_validate(response) - return GetAccessTokenFailureResponse.model_validate(response) diff --git a/src/workos/portal.py b/src/workos/portal.py deleted file mode 100644 index c5a08123..00000000 --- a/src/workos/portal.py +++ /dev/null @@ -1,65 +0,0 @@ -from typing import Optional, Protocol -from workos.types.portal.portal_link import PortalLink -from workos.types.portal.portal_link_intent import PortalLinkIntent -from workos.types.portal.portal_link_intent_options import IntentOptions -from workos.utils.http_client import SyncHTTPClient -from workos.utils.request_helper import REQUEST_METHOD_POST - - -PORTAL_GENERATE_PATH = "portal/generate_link" - - -class PortalModule(Protocol): - def generate_link( - self, - *, - intent: PortalLinkIntent, - organization_id: str, - return_url: Optional[str] = None, - success_url: Optional[str] = None, - intent_options: Optional[IntentOptions] = None, - ) -> PortalLink: - """Generate a link to grant access to an organization's Admin Portal - - Kwargs: - intent (PortalLinkIntent): The access scope for the generated Admin Portal link. - organization_id (str): The ID of the organization the Admin Portal link will be generated for. - return_url (str): The URL that the end user will be redirected to upon exiting the generated Admin Portal. - If none is provided, the default redirect link set in your WorkOS Dashboard will be used. (Optional) - success_url (str): The URL to which WorkOS will redirect users to upon successfully viewing Audit Logs, - setting up Log Streams, Single Sign On or Directory Sync. (Optional) - - Returns: - PortalLink: PortalLink object with URL to redirect a User to to access an Admin Portal session. - """ - ... - - -class Portal(PortalModule): - _http_client: SyncHTTPClient - - def __init__(self, http_client: SyncHTTPClient): - self._http_client = http_client - - def generate_link( - self, - *, - intent: PortalLinkIntent, - organization_id: str, - return_url: Optional[str] = None, - success_url: Optional[str] = None, - intent_options: Optional[IntentOptions] = None, - ) -> PortalLink: - json = { - "intent": intent, - "organization": organization_id, - "return_url": return_url, - "success_url": success_url, - "intent_options": intent_options, - } - - response = self._http_client.request( - PORTAL_GENERATE_PATH, method=REQUEST_METHOD_POST, json=json - ) - - return PortalLink.model_validate(response) diff --git a/src/workos/session.py b/src/workos/session.py deleted file mode 100644 index 0627430f..00000000 --- a/src/workos/session.py +++ /dev/null @@ -1,360 +0,0 @@ -from __future__ import annotations -from typing import TYPE_CHECKING, List, Protocol - -from functools import lru_cache -import json -from typing import Any, Dict, Optional, Union, cast - -import jwt -from jwt import PyJWKClient -from cryptography.fernet import Fernet - -from workos.types.user_management.session import ( - AuthenticateWithSessionCookieFailureReason, - AuthenticateWithSessionCookieSuccessResponse, - AuthenticateWithSessionCookieErrorResponse, - RefreshWithSessionCookieErrorResponse, - RefreshWithSessionCookieSuccessResponse, -) -from workos.typing.sync_or_async import SyncOrAsync - -if TYPE_CHECKING: - from workos.user_management import UserManagementModule - from workos.user_management import AsyncUserManagement, UserManagement - - -@lru_cache(maxsize=None) -def _get_jwks_client(jwks_url: str) -> PyJWKClient: - return PyJWKClient(jwks_url) - - -class SessionModule(Protocol): - user_management: "UserManagementModule" - client_id: str - session_data: str - cookie_password: str - jwks: PyJWKClient - jwk_algorithms: List[str] - jwt_leeway: float - - def __init__( - self, - *, - user_management: "UserManagementModule", - client_id: str, - session_data: str, - cookie_password: str, - jwt_leeway: float = 0, - ) -> None: - # If the cookie password is not provided, throw an error - if cookie_password is None or cookie_password == "": - raise ValueError("cookie_password is required") - - self.user_management = user_management - self.client_id = client_id - self.session_data = session_data - self.cookie_password = cookie_password - self.jwt_leeway = jwt_leeway - - self.jwks = _get_jwks_client(self.user_management.get_jwks_url()) - - # Algorithms are hardcoded for security reasons. See https://pyjwt.readthedocs.io/en/stable/algorithms.html#specifying-an-algorithm - self.jwk_algorithms = ["RS256"] - - def authenticate( - self, - ) -> Union[ - AuthenticateWithSessionCookieSuccessResponse, - AuthenticateWithSessionCookieErrorResponse, - ]: - if self.session_data is None or self.session_data == "": - return AuthenticateWithSessionCookieErrorResponse( - authenticated=False, - reason=AuthenticateWithSessionCookieFailureReason.NO_SESSION_COOKIE_PROVIDED, - ) - - try: - session = self.unseal_data(self.session_data, self.cookie_password) - except Exception: - return AuthenticateWithSessionCookieErrorResponse( - authenticated=False, - reason=AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE, - ) - - if not session.get("access_token", None): - return AuthenticateWithSessionCookieErrorResponse( - authenticated=False, - reason=AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE, - ) - - try: - signing_key = self.jwks.get_signing_key_from_jwt(session["access_token"]) - decoded = jwt.decode( - session["access_token"], - signing_key.key, - algorithms=self.jwk_algorithms, - options={"verify_aud": False}, - leeway=self.jwt_leeway, - ) - except jwt.exceptions.InvalidTokenError: - return AuthenticateWithSessionCookieErrorResponse( - authenticated=False, - reason=AuthenticateWithSessionCookieFailureReason.INVALID_JWT, - ) - return AuthenticateWithSessionCookieSuccessResponse( - authenticated=True, - session_id=decoded["sid"], - organization_id=decoded.get("org_id", None), - role=decoded.get("role", None), - roles=decoded.get("roles", None), - permissions=decoded.get("permissions", None), - entitlements=decoded.get("entitlements", None), - user=session["user"], - impersonator=session.get("impersonator", None), - feature_flags=decoded.get("feature_flags", None), - ) - - def refresh( - self, - *, - organization_id: Optional[str] = None, - cookie_password: Optional[str] = None, - ) -> SyncOrAsync[ - Union[ - RefreshWithSessionCookieSuccessResponse, - RefreshWithSessionCookieErrorResponse, - ] - ]: ... - - def get_logout_url(self, return_to: Optional[str] = None) -> str: - auth_response = self.authenticate() - - if isinstance(auth_response, AuthenticateWithSessionCookieErrorResponse): - raise ValueError( - f"Failed to extract session ID for logout URL: {auth_response.reason}" - ) - - result = self.user_management.get_logout_url( - session_id=auth_response.session_id, - return_to=return_to, - ) - return str(result) - - def _is_valid_jwt(self, token: str) -> bool: - try: - signing_key = self.jwks.get_signing_key_from_jwt(token) - jwt.decode( - token, - signing_key.key, - algorithms=self.jwk_algorithms, - options={"verify_aud": False}, - leeway=self.jwt_leeway, - ) - return True - except jwt.exceptions.InvalidTokenError: - return False - - @staticmethod - def seal_data(data: Dict[str, Any], key: str) -> str: - fernet = Fernet(key) - # Encrypt and convert bytes to string - encrypted_bytes = fernet.encrypt(json.dumps(data).encode()) - return encrypted_bytes.decode("utf-8") - - @staticmethod - def unseal_data(sealed_data: str, key: str) -> Dict[str, Any]: - fernet = Fernet(key) - # Convert string back to bytes before decryption - encrypted_bytes = sealed_data.encode("utf-8") - decrypted_str = fernet.decrypt(encrypted_bytes).decode() - return cast(Dict[str, Any], json.loads(decrypted_str)) - - -class Session(SessionModule): - user_management: "UserManagement" - - def __init__( - self, - *, - user_management: "UserManagement", - client_id: str, - session_data: str, - cookie_password: str, - jwt_leeway: float = 0, - ) -> None: - # If the cookie password is not provided, throw an error - if cookie_password is None or cookie_password == "": - raise ValueError("cookie_password is required") - - self.user_management = user_management - self.client_id = client_id - self.session_data = session_data - self.cookie_password = cookie_password - self.jwt_leeway = jwt_leeway - - self.jwks = _get_jwks_client(self.user_management.get_jwks_url()) - - # Algorithms are hardcoded for security reasons. See https://pyjwt.readthedocs.io/en/stable/algorithms.html#specifying-an-algorithm - self.jwk_algorithms = ["RS256"] - - def refresh( - self, - *, - organization_id: Optional[str] = None, - cookie_password: Optional[str] = None, - ) -> Union[ - RefreshWithSessionCookieSuccessResponse, - RefreshWithSessionCookieErrorResponse, - ]: - cookie_password = ( - self.cookie_password if cookie_password is None else cookie_password - ) - - try: - session = self.unseal_data(self.session_data, cookie_password) - except Exception: - return RefreshWithSessionCookieErrorResponse( - authenticated=False, - reason=AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE, - ) - - if not session.get("refresh_token", None) or not session.get("user", None): - return RefreshWithSessionCookieErrorResponse( - authenticated=False, - reason=AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE, - ) - - try: - auth_response = self.user_management.authenticate_with_refresh_token( - refresh_token=session["refresh_token"], - organization_id=organization_id, - session={"seal_session": True, "cookie_password": cookie_password}, - ) - - self.session_data = str(auth_response.sealed_session) - self.cookie_password = ( - cookie_password if cookie_password is not None else self.cookie_password - ) - - signing_key = self.jwks.get_signing_key_from_jwt(auth_response.access_token) - - decoded = jwt.decode( - auth_response.access_token, - signing_key.key, - algorithms=self.jwk_algorithms, - options={"verify_aud": False}, - leeway=self.jwt_leeway, - ) - - return RefreshWithSessionCookieSuccessResponse( - authenticated=True, - sealed_session=str(auth_response.sealed_session), - session_id=decoded["sid"], - organization_id=decoded.get("org_id", None), - role=decoded.get("role", None), - roles=decoded.get("roles", None), - permissions=decoded.get("permissions", None), - entitlements=decoded.get("entitlements", None), - user=auth_response.user, - impersonator=auth_response.impersonator, - feature_flags=decoded.get("feature_flags", None), - ) - except Exception as e: - return RefreshWithSessionCookieErrorResponse( - authenticated=False, reason=str(e) - ) - - -class AsyncSession(SessionModule): - user_management: "AsyncUserManagement" - - def __init__( - self, - *, - user_management: "AsyncUserManagement", - client_id: str, - session_data: str, - cookie_password: str, - jwt_leeway: float = 0, - ) -> None: - # If the cookie password is not provided, throw an error - if cookie_password is None or cookie_password == "": - raise ValueError("cookie_password is required") - - self.user_management = user_management - self.client_id = client_id - self.session_data = session_data - self.cookie_password = cookie_password - self.jwt_leeway = jwt_leeway - - self.jwks = _get_jwks_client(self.user_management.get_jwks_url()) - - # Algorithms are hardcoded for security reasons. See https://pyjwt.readthedocs.io/en/stable/algorithms.html#specifying-an-algorithm - self.jwk_algorithms = ["RS256"] - - async def refresh( - self, - *, - organization_id: Optional[str] = None, - cookie_password: Optional[str] = None, - ) -> Union[ - RefreshWithSessionCookieSuccessResponse, - RefreshWithSessionCookieErrorResponse, - ]: - cookie_password = ( - self.cookie_password if cookie_password is None else cookie_password - ) - - try: - session = self.unseal_data(self.session_data, cookie_password) - except Exception: - return RefreshWithSessionCookieErrorResponse( - authenticated=False, - reason=AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE, - ) - - if not session.get("refresh_token", None) or not session.get("user", None): - return RefreshWithSessionCookieErrorResponse( - authenticated=False, - reason=AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE, - ) - - try: - auth_response = await self.user_management.authenticate_with_refresh_token( - refresh_token=session["refresh_token"], - organization_id=organization_id, - session={"seal_session": True, "cookie_password": cookie_password}, - ) - - self.session_data = str(auth_response.sealed_session) - self.cookie_password = ( - cookie_password if cookie_password is not None else self.cookie_password - ) - - signing_key = self.jwks.get_signing_key_from_jwt(auth_response.access_token) - - decoded = jwt.decode( - auth_response.access_token, - signing_key.key, - algorithms=self.jwk_algorithms, - options={"verify_aud": False}, - leeway=self.jwt_leeway, - ) - - return RefreshWithSessionCookieSuccessResponse( - authenticated=True, - sealed_session=str(auth_response.sealed_session), - session_id=decoded["sid"], - organization_id=decoded.get("org_id", None), - role=decoded.get("role", None), - roles=decoded.get("roles", None), - permissions=decoded.get("permissions", None), - entitlements=decoded.get("entitlements", None), - user=auth_response.user, - impersonator=auth_response.impersonator, - feature_flags=decoded.get("feature_flags", None), - ) - except Exception as e: - return RefreshWithSessionCookieErrorResponse( - authenticated=False, reason=str(e) - ) diff --git a/src/workos/sso.py b/src/workos/sso.py deleted file mode 100644 index 7fa722a6..00000000 --- a/src/workos/sso.py +++ /dev/null @@ -1,405 +0,0 @@ -from typing import Optional, Protocol -from workos._client_configuration import ClientConfiguration -from workos.types.sso.connection import ConnectionType -from workos.types.sso.sso_provider_type import SsoProviderType -from workos.typing.sync_or_async import SyncOrAsync -from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient -from workos.utils.pagination_order import PaginationOrder -from workos.types.sso import ConnectionWithDomains, Profile, ProfileAndToken -from workos.utils.request_helper import ( - DEFAULT_LIST_RESPONSE_LIMIT, - RESPONSE_TYPE_CODE, - REQUEST_METHOD_DELETE, - REQUEST_METHOD_GET, - REQUEST_METHOD_POST, - QueryParameters, - RequestHelper, - REQUEST_METHOD_PUT, -) -from workos.types.list_resource import ( - ListArgs, - ListMetadata, - ListPage, - WorkOSListResource, -) - -AUTHORIZATION_PATH = "sso/authorize" -TOKEN_PATH = "sso/token" -PROFILE_PATH = "sso/profile" - -OAUTH_GRANT_TYPE = "authorization_code" - - -class ConnectionsListFilters(ListArgs, total=False): - connection_type: Optional[ConnectionType] - domain: Optional[str] - organization_id: Optional[str] - - -ConnectionsListResource = WorkOSListResource[ - ConnectionWithDomains, ConnectionsListFilters, ListMetadata -] - - -class SSOModule(Protocol): - """Offers methods to assist in authenticating through the WorkOS SSO service.""" - - _client_configuration: ClientConfiguration - - def get_authorization_url( - self, - *, - redirect_uri: str, - domain_hint: Optional[str] = None, - login_hint: Optional[str] = None, - state: Optional[str] = None, - provider: Optional[SsoProviderType] = None, - connection_id: Optional[str] = None, - organization_id: Optional[str] = None, - ) -> str: - """Generate an OAuth 2.0 authorization URL. - - The URL generated will redirect a User to the Identity Provider configured through - WorkOS. - - This method is purposefully designed as synchronous as it does not make any HTTP requests. - - Kwargs: - redirect_uri (str) : A valid redirect URI, as specified on WorkOS - state (str) : An encoded string passed to WorkOS that'd be preserved through the authentication workflow, passed - back as a query parameter - provider (SSOProviderType) : Authentication service provider descriptor - connection_id (string) : Unique identifier for a WorkOS Connection - organization_id (string) : Unique identifier for a WorkOS Organization - - Returns: - str: URL to redirect a User to to begin the OAuth workflow with WorkOS - """ - params: QueryParameters = { - "client_id": self._client_configuration.client_id, - "redirect_uri": redirect_uri, - "response_type": RESPONSE_TYPE_CODE, - } - - if connection_id is None and organization_id is None and provider is None: - raise ValueError( - "Incomplete arguments. Need to specify either a 'connection', 'organization', or 'provider'" - ) - if provider is not None: - params["provider"] = provider - if domain_hint is not None: - params["domain_hint"] = domain_hint - if login_hint is not None: - params["login_hint"] = login_hint - if connection_id is not None: - params["connection"] = connection_id - if organization_id is not None: - params["organization"] = organization_id - - if state is not None: - params["state"] = state - - return RequestHelper.build_url_with_query_params( - base_url=self._client_configuration.base_url, - path=AUTHORIZATION_PATH, - **params, - ) - - def get_profile(self, access_token: str) -> SyncOrAsync[Profile]: - """ - Verify that SSO has been completed successfully and retrieve the identity of the user. - - Args: - access_token (str): The token used to authenticate the API call - - Returns: - Profile - """ - ... - - def get_profile_and_token(self, code: str) -> SyncOrAsync[ProfileAndToken]: - """Get the profile of an authenticated User - - Once authenticated, using the code returned having followed the authorization URL, - get the WorkOS profile of the User. - - Args: - code (str): Code returned by WorkOS on completion of OAuth 2.0 workflow. - - Returns: - ProfileAndToken: WorkOSProfileAndToken object representing the User. - """ - ... - - def get_connection(self, connection_id: str) -> SyncOrAsync[ConnectionWithDomains]: - """Gets details for a single Connection - - Args: - connection (str): Connection unique identifier - - Returns: - ConnectionWithDomains: Connection response from WorkOS. - """ - ... - - def list_connections( - self, - *, - connection_type: Optional[ConnectionType] = None, - domain: Optional[str] = None, - organization_id: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[ConnectionsListResource]: - """Gets details for existing Connections. - - Kwargs: - connection_type (ConnectionType): Authentication service provider descriptor. (Optional) - domain (str): Domain of a Connection. (Optional) - limit (int): Maximum number of records to return. (Optional) - before (str): Pagination cursor to receive records before a provided Connection ID. (Optional) - after (str): Pagination cursor to receive records after a provided Connection ID. (Optional) - order (Literal["asc","desc"]): Sort records in either ascending or descending (default) order by created_at timestamp. (Optional) - - Returns: - ConnectionsListResource: Connections response from WorkOS. - """ - ... - - def update_connection( - self, - *, - connection_id: str, - saml_options_signing_key: Optional[str] = None, - saml_options_signing_cert: Optional[str] = None, - ) -> SyncOrAsync[ConnectionWithDomains]: - """Updates a single connection - - Args: - connection_id (str): Connection unique identifier - saml_options_signing_key (str): Signing key for the connection (Optional) - saml_options_signing_cert (str): Signing certificate for the connection (Optional) - Returns: - None - """ - ... - - def delete_connection(self, connection_id: str) -> SyncOrAsync[None]: - """Deletes a single Connection - - Args: - connection_id (str): Connection unique identifier - - Returns: - None - """ - ... - - -class SSO(SSOModule): - _http_client: SyncHTTPClient - - def __init__( - self, http_client: SyncHTTPClient, client_configuration: ClientConfiguration - ): - self._client_configuration = client_configuration - self._http_client = http_client - - def get_profile(self, access_token: str) -> Profile: - response = self._http_client.request( - PROFILE_PATH, - method=REQUEST_METHOD_GET, - headers={**self._http_client.auth_header_from_token(access_token)}, - exclude_default_auth_headers=True, - ) - - return Profile.model_validate(response) - - def get_profile_and_token(self, code: str) -> ProfileAndToken: - json = { - "client_id": self._http_client.client_id, - "client_secret": self._http_client.api_key, - "code": code, - "grant_type": OAUTH_GRANT_TYPE, - } - - response = self._http_client.request( - TOKEN_PATH, method=REQUEST_METHOD_POST, json=json - ) - - return ProfileAndToken.model_validate(response) - - def get_connection(self, connection_id: str) -> ConnectionWithDomains: - response = self._http_client.request( - f"connections/{connection_id}", - method=REQUEST_METHOD_GET, - ) - - return ConnectionWithDomains.model_validate(response) - - def list_connections( - self, - *, - connection_type: Optional[ConnectionType] = None, - domain: Optional[str] = None, - organization_id: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> ConnectionsListResource: - params: ConnectionsListFilters = { - "connection_type": connection_type, - "domain": domain, - "organization_id": organization_id, - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = self._http_client.request( - "connections", - method=REQUEST_METHOD_GET, - params=params, - ) - - return WorkOSListResource[ - ConnectionWithDomains, ConnectionsListFilters, ListMetadata - ]( - list_method=self.list_connections, - list_args=params, - **ListPage[ConnectionWithDomains](**response).model_dump(), - ) - - def update_connection( - self, - *, - connection_id: str, - saml_options_signing_key: Optional[str] = None, - saml_options_signing_cert: Optional[str] = None, - ) -> ConnectionWithDomains: - json = { - "options": { - "signing_key": saml_options_signing_key, - "signing_cert": saml_options_signing_cert, - } - } - - response = self._http_client.request( - f"connections/{connection_id}", - method=REQUEST_METHOD_PUT, - json=json, - ) - - return ConnectionWithDomains.model_validate(response) - - def delete_connection(self, connection_id: str) -> None: - self._http_client.request( - f"connections/{connection_id}", method=REQUEST_METHOD_DELETE - ) - - -class AsyncSSO(SSOModule): - _http_client: AsyncHTTPClient - - def __init__( - self, http_client: AsyncHTTPClient, client_configuration: ClientConfiguration - ): - self._client_configuration = client_configuration - self._http_client = http_client - - async def get_profile(self, access_token: str) -> Profile: - response = await self._http_client.request( - PROFILE_PATH, - method=REQUEST_METHOD_GET, - headers={**self._http_client.auth_header_from_token(access_token)}, - exclude_default_auth_headers=True, - ) - - return Profile.model_validate(response) - - async def get_profile_and_token(self, code: str) -> ProfileAndToken: - json = { - "client_id": self._http_client.client_id, - "client_secret": self._http_client.api_key, - "code": code, - "grant_type": OAUTH_GRANT_TYPE, - } - - response = await self._http_client.request( - TOKEN_PATH, method=REQUEST_METHOD_POST, json=json - ) - - return ProfileAndToken.model_validate(response) - - async def get_connection(self, connection_id: str) -> ConnectionWithDomains: - response = await self._http_client.request( - f"connections/{connection_id}", - method=REQUEST_METHOD_GET, - ) - - return ConnectionWithDomains.model_validate(response) - - async def list_connections( - self, - *, - connection_type: Optional[ConnectionType] = None, - domain: Optional[str] = None, - organization_id: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> ConnectionsListResource: - params: ConnectionsListFilters = { - "connection_type": connection_type, - "domain": domain, - "organization_id": organization_id, - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = await self._http_client.request( - "connections", method=REQUEST_METHOD_GET, params=params - ) - - return WorkOSListResource[ - ConnectionWithDomains, ConnectionsListFilters, ListMetadata - ]( - list_method=self.list_connections, - list_args=params, - **ListPage[ConnectionWithDomains](**response).model_dump(), - ) - - async def update_connection( - self, - *, - connection_id: str, - saml_options_signing_key: Optional[str] = None, - saml_options_signing_cert: Optional[str] = None, - ) -> ConnectionWithDomains: - json = { - "options": { - "signing_key": saml_options_signing_key, - "signing_cert": saml_options_signing_cert, - } - } - - response = await self._http_client.request( - f"connections/{connection_id}", - method=REQUEST_METHOD_PUT, - json=json, - ) - - return ConnectionWithDomains.model_validate(response) - - async def delete_connection(self, connection_id: str) -> None: - await self._http_client.request( - f"connections/{connection_id}", - method=REQUEST_METHOD_DELETE, - ) diff --git a/src/workos/types/__init__.py b/src/workos/types/__init__.py deleted file mode 100644 index ab603c38..00000000 --- a/src/workos/types/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .audit_logs.audit_log_event import AuditLogEvent as AuditLogEvent -from .organizations.domain_data_input import DomainDataInput as DomainDataInput -from .fga.warrant import WarrantWrite as WarrantWrite -from .fga.check import WarrantCheckInput as WarrantCheckInput diff --git a/src/workos/types/api_keys/__init__.py b/src/workos/types/api_keys/__init__.py deleted file mode 100644 index 41e7176a..00000000 --- a/src/workos/types/api_keys/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .api_keys import ApiKey as ApiKey # noqa: F401 -from .api_keys import ApiKeyWithValue as ApiKeyWithValue # noqa: F401 diff --git a/src/workos/types/api_keys/api_keys.py b/src/workos/types/api_keys/api_keys.py deleted file mode 100644 index 2d4f6970..00000000 --- a/src/workos/types/api_keys/api_keys.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Literal, Optional, Sequence - -from workos.types.workos_model import WorkOSModel - - -class ApiKeyOwner(WorkOSModel): - type: str - id: str - - -class ApiKey(WorkOSModel): - object: Literal["api_key"] - id: str - owner: ApiKeyOwner - name: str - obfuscated_value: str - last_used_at: Optional[str] = None - permissions: Sequence[str] - created_at: str - updated_at: str - - -class ApiKeyWithValue(ApiKey): - """API key with the full value field, returned only on creation.""" - - value: str diff --git a/src/workos/types/api_keys/list_filters.py b/src/workos/types/api_keys/list_filters.py deleted file mode 100644 index cfdf7b53..00000000 --- a/src/workos/types/api_keys/list_filters.py +++ /dev/null @@ -1,5 +0,0 @@ -from workos.types.list_resource import ListArgs - - -class ApiKeyListFilters(ListArgs, total=False): - pass diff --git a/src/workos/types/audit_logs/__init__.py b/src/workos/types/audit_logs/__init__.py deleted file mode 100644 index 6f36daea..00000000 --- a/src/workos/types/audit_logs/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .audit_log_action import * -from .audit_log_configuration import * -from .audit_log_event_actor import * -from .audit_log_event_context import * -from .audit_log_event_target import * -from .audit_log_event import * -from .audit_log_export import * -from .audit_log_metadata import * -from .audit_log_retention import * -from .audit_log_schema import * -from .audit_log_schema_input import * -from .list_filters import * diff --git a/src/workos/types/audit_logs/audit_log_action.py b/src/workos/types/audit_logs/audit_log_action.py deleted file mode 100644 index a342f143..00000000 --- a/src/workos/types/audit_logs/audit_log_action.py +++ /dev/null @@ -1,28 +0,0 @@ -import warnings -from typing import Literal - -from workos.types.audit_logs.audit_log_schema import AuditLogSchema -from workos.types.workos_model import WorkOSModel - -# Suppress Pydantic warning about 'schema' shadowing BaseModel.schema() -# (a deprecated method replaced by model_json_schema() in Pydantic v2) -warnings.filterwarnings( - "ignore", - message='Field name "schema" in "AuditLogAction" shadows an attribute', - category=UserWarning, -) - - -class AuditLogAction(WorkOSModel): - """Representation of a WorkOS audit log action. - - An audit log action represents a configured action type that can be - used in audit log events. Each action has an associated schema that - defines the structure of events for that action. - """ - - object: Literal["audit_log_action"] - name: str - schema: AuditLogSchema # type: ignore[assignment] - created_at: str - updated_at: str diff --git a/src/workos/types/audit_logs/audit_log_configuration.py b/src/workos/types/audit_logs/audit_log_configuration.py deleted file mode 100644 index 3bff5a03..00000000 --- a/src/workos/types/audit_logs/audit_log_configuration.py +++ /dev/null @@ -1,41 +0,0 @@ -from typing import Literal, Optional - -from workos.types.workos_model import WorkOSModel -from workos.typing.literals import LiteralOrUntyped - - -AuditLogStreamType = Literal[ - "Datadog", "Splunk", "S3", "GoogleCloudStorage", "GenericHttps" -] - -AuditLogStreamState = Literal["active", "inactive", "error", "invalid"] - -AuditLogTrailState = Literal["active", "inactive", "disabled"] - - -class AuditLogStream(WorkOSModel): - """Representation of a WorkOS audit log stream. - - An audit log stream sends audit log events to an external destination - such as Datadog, Splunk, S3, Google Cloud Storage, or a custom HTTPS endpoint. - """ - - id: str - type: LiteralOrUntyped[AuditLogStreamType] - state: LiteralOrUntyped[AuditLogStreamState] - last_synced_at: Optional[str] = None - created_at: str - - -class AuditLogConfiguration(WorkOSModel): - """Representation of a WorkOS audit log configuration for an organization. - - The audit log configuration provides a single view of an organization's - audit logging setup, including retention settings, state, and optional - log stream configuration. - """ - - organization_id: str - retention_period_in_days: int - state: LiteralOrUntyped[AuditLogTrailState] - log_stream: Optional[AuditLogStream] = None diff --git a/src/workos/types/audit_logs/audit_log_event.py b/src/workos/types/audit_logs/audit_log_event.py deleted file mode 100644 index baf81d6d..00000000 --- a/src/workos/types/audit_logs/audit_log_event.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing_extensions import NotRequired, Sequence, TypedDict - -from workos.types.audit_logs.audit_log_event_actor import AuditLogEventActor -from workos.types.audit_logs.audit_log_event_context import AuditLogEventContext -from workos.types.audit_logs.audit_log_metadata import AuditLogMetadata -from workos.types.audit_logs.audit_log_event_target import AuditLogEventTarget - - -class AuditLogEvent(TypedDict): - action: str - version: NotRequired[int] - occurred_at: str # ISO-8601 datetime of when an event occurred - actor: AuditLogEventActor - targets: Sequence[AuditLogEventTarget] - context: AuditLogEventContext - metadata: NotRequired[AuditLogMetadata] diff --git a/src/workos/types/audit_logs/audit_log_event_actor.py b/src/workos/types/audit_logs/audit_log_event_actor.py deleted file mode 100644 index a231fa05..00000000 --- a/src/workos/types/audit_logs/audit_log_event_actor.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing_extensions import NotRequired, TypedDict - -from workos.types.audit_logs.audit_log_metadata import AuditLogMetadata - - -class AuditLogEventActor(TypedDict): - """Describes the entity that generated the event.""" - - id: str - metadata: NotRequired[AuditLogMetadata] - name: NotRequired[str] - type: str diff --git a/src/workos/types/audit_logs/audit_log_event_context.py b/src/workos/types/audit_logs/audit_log_event_context.py deleted file mode 100644 index aad104f0..00000000 --- a/src/workos/types/audit_logs/audit_log_event_context.py +++ /dev/null @@ -1,8 +0,0 @@ -from typing_extensions import NotRequired, TypedDict - - -class AuditLogEventContext(TypedDict): - """Attributes of audit log event context.""" - - location: str - user_agent: NotRequired[str] diff --git a/src/workos/types/audit_logs/audit_log_event_target.py b/src/workos/types/audit_logs/audit_log_event_target.py deleted file mode 100644 index 9ae2f852..00000000 --- a/src/workos/types/audit_logs/audit_log_event_target.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing_extensions import NotRequired, TypedDict - -from workos.types.audit_logs.audit_log_metadata import AuditLogMetadata - - -class AuditLogEventTarget(TypedDict): - """Describes the entity that was targeted by the event.""" - - id: str - metadata: NotRequired[AuditLogMetadata] - name: NotRequired[str] - type: str diff --git a/src/workos/types/audit_logs/audit_log_export.py b/src/workos/types/audit_logs/audit_log_export.py deleted file mode 100644 index 1cc7436e..00000000 --- a/src/workos/types/audit_logs/audit_log_export.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Literal, Optional - -from workos.types.workos_model import WorkOSModel -from workos.typing.literals import LiteralOrUntyped - - -AuditLogExportState = Literal["error", "pending", "ready"] - - -class AuditLogExport(WorkOSModel): - """Representation of a WorkOS audit logs export.""" - - object: Literal["audit_log_export"] - id: str - created_at: str - updated_at: str - state: LiteralOrUntyped[AuditLogExportState] - url: Optional[str] = None diff --git a/src/workos/types/audit_logs/audit_log_metadata.py b/src/workos/types/audit_logs/audit_log_metadata.py deleted file mode 100644 index f8d1e069..00000000 --- a/src/workos/types/audit_logs/audit_log_metadata.py +++ /dev/null @@ -1,4 +0,0 @@ -from typing import Any, Mapping - - -AuditLogMetadata = Mapping[str, Any] diff --git a/src/workos/types/audit_logs/audit_log_retention.py b/src/workos/types/audit_logs/audit_log_retention.py deleted file mode 100644 index 70f5b4d3..00000000 --- a/src/workos/types/audit_logs/audit_log_retention.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Optional - -from workos.types.workos_model import WorkOSModel - - -class AuditLogRetention(WorkOSModel): - """Representation of a WorkOS audit log retention configuration. - - Specifies how long audit log events are retained for an organization. - Valid values are 30 and 365 days, or None if not configured. - """ - - retention_period_in_days: Optional[int] = None diff --git a/src/workos/types/audit_logs/audit_log_schema.py b/src/workos/types/audit_logs/audit_log_schema.py deleted file mode 100644 index a34427fa..00000000 --- a/src/workos/types/audit_logs/audit_log_schema.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import Dict, Literal, Optional, Sequence - -from workos.types.workos_model import WorkOSModel - - -class AuditLogSchemaMetadataProperty(WorkOSModel): - """A property definition within an audit log schema metadata object.""" - - type: Literal["string", "boolean", "number"] - - -class AuditLogSchemaMetadata(WorkOSModel): - """The metadata definition for an audit log schema. - - Represents a JSON Schema object type with property definitions. - """ - - type: Literal["object"] - properties: Optional[Dict[str, AuditLogSchemaMetadataProperty]] = None - - -class AuditLogSchemaTarget(WorkOSModel): - """A target definition within an audit log schema.""" - - type: str - metadata: Optional[AuditLogSchemaMetadata] = None - - -class AuditLogSchemaActor(WorkOSModel): - """The actor definition within an audit log schema.""" - - metadata: AuditLogSchemaMetadata - - -class AuditLogSchema(WorkOSModel): - """Representation of a WorkOS audit log schema. - - Audit log schemas define the structure and validation rules - for audit log events, including the allowed targets, actor metadata, - and event-level metadata. - """ - - object: Literal["audit_log_schema"] = "audit_log_schema" - version: int - targets: Sequence[AuditLogSchemaTarget] - actor: Optional[AuditLogSchemaActor] = None - metadata: Optional[AuditLogSchemaMetadata] = None - created_at: Optional[str] = None - updated_at: Optional[str] = None diff --git a/src/workos/types/audit_logs/audit_log_schema_input.py b/src/workos/types/audit_logs/audit_log_schema_input.py deleted file mode 100644 index ebba0f25..00000000 --- a/src/workos/types/audit_logs/audit_log_schema_input.py +++ /dev/null @@ -1,78 +0,0 @@ -from typing import Any, Dict, Literal, Mapping, Optional, Sequence - -from typing_extensions import NotRequired, TypedDict - -MetadataSchemaInput = Mapping[str, Literal["string", "number", "boolean"]] - - -class AuditLogSchemaTargetInput(TypedDict): - """Input type for target definitions when creating an audit log schema. - - Attributes: - type: The target type identifier (e.g., "team", "user", "document"). - metadata: Optional simplified metadata schema mapping property names to types. - """ - - type: str - metadata: NotRequired[MetadataSchemaInput] - - -class AuditLogSchemaActorInput(TypedDict): - """Input type for actor definition when creating an audit log schema. - - Attributes: - metadata: Simplified metadata schema mapping property names to types. - """ - - metadata: MetadataSchemaInput - - -def _serialize_metadata( - metadata: Optional[MetadataSchemaInput], -) -> Optional[Dict[str, Any]]: - """Transform simplified metadata to full JSON Schema format. - - Transforms {"role": "string"} to: - {"type": "object", "properties": {"role": {"type": "string"}}} - """ - if not metadata: - return None - - properties: Dict[str, Dict[str, str]] = {} - for key, type_value in metadata.items(): - properties[key] = {"type": type_value} - - return {"type": "object", "properties": properties} - - -def serialize_schema_options( - targets: Sequence[AuditLogSchemaTargetInput], - actor: Optional[AuditLogSchemaActorInput] = None, - metadata: Optional[MetadataSchemaInput] = None, -) -> Dict[str, Any]: - """Serialize schema options from simplified format to API format. - - Transforms the simplified input format (matching JS SDK ergonomics) - to the full JSON Schema format expected by the API. - """ - result: Dict[str, Any] = { - "targets": [ - { - "type": target["type"], - **( - {"metadata": _serialize_metadata(target.get("metadata"))} - if target.get("metadata") - else {} - ), - } - for target in targets - ], - } - - if actor is not None: - result["actor"] = {"metadata": _serialize_metadata(actor["metadata"])} - - if metadata is not None: - result["metadata"] = _serialize_metadata(metadata) - - return result diff --git a/src/workos/types/audit_logs/list_filters.py b/src/workos/types/audit_logs/list_filters.py deleted file mode 100644 index 705c7cc6..00000000 --- a/src/workos/types/audit_logs/list_filters.py +++ /dev/null @@ -1,13 +0,0 @@ -from workos.types.list_resource import ListArgs - - -class AuditLogActionListFilters(ListArgs, total=False): - """Filters for listing audit log actions.""" - - pass - - -class AuditLogSchemaListFilters(ListArgs, total=False): - """Filters for listing audit log schemas.""" - - pass diff --git a/src/workos/types/authorization/__init__.py b/src/workos/types/authorization/__init__.py deleted file mode 100644 index 0098a54b..00000000 --- a/src/workos/types/authorization/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -from workos.types.authorization.access_check_response import AccessCheckResponse -from workos.types.authorization.assignment import Assignment -from workos.types.authorization.environment_role import ( - EnvironmentRole, - EnvironmentRoleList, -) -from workos.types.authorization.organization_membership import ( - AuthorizationOrganizationMembership, -) -from workos.types.authorization.organization_role import ( - OrganizationRole, - OrganizationRoleEvent, - OrganizationRoleList, -) -from workos.types.authorization.permission import Permission -from workos.types.authorization.parent_resource_identifier import ( - ParentResourceIdentifier, -) -from workos.types.authorization.authorization_resource import AuthorizationResource -from workos.types.authorization.resource_identifier import ( - ResourceIdentifier, - ResourceIdentifierByExternalId, - ResourceIdentifierById, -) -from workos.types.authorization.role import ( - Role, - RoleList, -) -from workos.types.authorization.role_assignment import ( - RoleAssignment, - RoleAssignmentResource, - RoleAssignmentRole, -) diff --git a/src/workos/types/authorization/access_check_response.py b/src/workos/types/authorization/access_check_response.py deleted file mode 100644 index 2515b763..00000000 --- a/src/workos/types/authorization/access_check_response.py +++ /dev/null @@ -1,5 +0,0 @@ -from workos.types.workos_model import WorkOSModel - - -class AccessCheckResponse(WorkOSModel): - authorized: bool diff --git a/src/workos/types/authorization/assignment.py b/src/workos/types/authorization/assignment.py deleted file mode 100644 index ea87fca9..00000000 --- a/src/workos/types/authorization/assignment.py +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Literal - -Assignment = Literal["direct", "indirect"] diff --git a/src/workos/types/authorization/authorization_resource.py b/src/workos/types/authorization/authorization_resource.py deleted file mode 100644 index ea722f9b..00000000 --- a/src/workos/types/authorization/authorization_resource.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Literal, Optional - -from workos.types.workos_model import WorkOSModel - - -class AuthorizationResource(WorkOSModel): - """Representation of an Authorization Resource.""" - - object: Literal["authorization_resource"] - id: str - external_id: str - name: str - description: Optional[str] = None - resource_type_slug: str - organization_id: str - parent_resource_id: Optional[str] = None - created_at: str - updated_at: str diff --git a/src/workos/types/authorization/environment_role.py b/src/workos/types/authorization/environment_role.py deleted file mode 100644 index 750de5ca..00000000 --- a/src/workos/types/authorization/environment_role.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Literal, Optional, Sequence - -from workos.types.workos_model import WorkOSModel - - -class EnvironmentRole(WorkOSModel): - object: Literal["role"] - id: str - name: str - slug: str - description: Optional[str] = None - resource_type_slug: str - permissions: Sequence[str] - type: Literal["EnvironmentRole"] - created_at: str - updated_at: str - - -class EnvironmentRoleList(WorkOSModel): - object: Literal["list"] - data: Sequence[EnvironmentRole] diff --git a/src/workos/types/authorization/organization_membership.py b/src/workos/types/authorization/organization_membership.py deleted file mode 100644 index f24e9a27..00000000 --- a/src/workos/types/authorization/organization_membership.py +++ /dev/null @@ -1,5 +0,0 @@ -from workos.types.user_management.organization_membership import ( - BaseOrganizationMembership, -) - -AuthorizationOrganizationMembership = BaseOrganizationMembership diff --git a/src/workos/types/authorization/organization_role.py b/src/workos/types/authorization/organization_role.py deleted file mode 100644 index dbece55d..00000000 --- a/src/workos/types/authorization/organization_role.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Literal, Optional, Sequence - -from workos.types.workos_model import WorkOSModel - - -class OrganizationRole(WorkOSModel): - object: Literal["role"] - id: str - name: str - slug: str - description: Optional[str] = None - resource_type_slug: str - permissions: Sequence[str] - type: Literal["OrganizationRole"] - created_at: str - updated_at: str - - -class OrganizationRoleEvent(WorkOSModel): - """Organization role type for Events API responses.""" - - object: Literal["organization_role"] - organization_id: str - slug: str - name: str - description: Optional[str] = None - resource_type_slug: str - permissions: Sequence[str] - created_at: str - updated_at: str - - -class OrganizationRoleList(WorkOSModel): - object: Literal["list"] - data: Sequence[OrganizationRole] diff --git a/src/workos/types/authorization/parent_resource_identifier.py b/src/workos/types/authorization/parent_resource_identifier.py deleted file mode 100644 index c105f87b..00000000 --- a/src/workos/types/authorization/parent_resource_identifier.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Union - -from typing_extensions import TypedDict - - -class ParentResourceById(TypedDict): - parent_resource_id: str - - -class ParentResourceByExternalId(TypedDict): - parent_resource_external_id: str - parent_resource_type_slug: str - - -ParentResourceIdentifier = Union[ParentResourceById, ParentResourceByExternalId] diff --git a/src/workos/types/authorization/permission.py b/src/workos/types/authorization/permission.py deleted file mode 100644 index 516d4f73..00000000 --- a/src/workos/types/authorization/permission.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Literal, Optional - -from workos.types.workos_model import WorkOSModel - - -class Permission(WorkOSModel): - object: Literal["permission"] - id: str - slug: str - name: str - description: Optional[str] = None - resource_type_slug: str - system: bool - created_at: str - updated_at: str diff --git a/src/workos/types/authorization/resource_identifier.py b/src/workos/types/authorization/resource_identifier.py deleted file mode 100644 index 081a175d..00000000 --- a/src/workos/types/authorization/resource_identifier.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Union - -from typing_extensions import TypedDict - - -class ResourceIdentifierById(TypedDict): - resource_id: str - - -class ResourceIdentifierByExternalId(TypedDict): - resource_external_id: str - resource_type_slug: str - - -ResourceIdentifier = Union[ResourceIdentifierById, ResourceIdentifierByExternalId] diff --git a/src/workos/types/authorization/role.py b/src/workos/types/authorization/role.py deleted file mode 100644 index 7ea6f747..00000000 --- a/src/workos/types/authorization/role.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Literal, Sequence, Union - -from pydantic import Field -from typing_extensions import Annotated - -from workos.types.authorization.environment_role import EnvironmentRole -from workos.types.authorization.organization_role import OrganizationRole -from workos.types.workos_model import WorkOSModel - -Role = Annotated[ - Union[EnvironmentRole, OrganizationRole], - Field(discriminator="type"), -] - - -class RoleList(WorkOSModel): - object: Literal["list"] - data: Sequence[Role] diff --git a/src/workos/types/authorization/role_assignment.py b/src/workos/types/authorization/role_assignment.py deleted file mode 100644 index 9ca59936..00000000 --- a/src/workos/types/authorization/role_assignment.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import Literal - -from workos.types.workos_model import WorkOSModel - - -class RoleAssignmentRole(WorkOSModel): - slug: str - - -class RoleAssignmentResource(WorkOSModel): - id: str - external_id: str - resource_type_slug: str - - -class RoleAssignment(WorkOSModel): - object: Literal["role_assignment"] - id: str - role: RoleAssignmentRole - resource: RoleAssignmentResource - created_at: str - updated_at: str diff --git a/src/workos/types/connect/__init__.py b/src/workos/types/connect/__init__.py deleted file mode 100644 index 87fce74b..00000000 --- a/src/workos/types/connect/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .connect_application import * -from .client_secret import * diff --git a/src/workos/types/connect/client_secret.py b/src/workos/types/connect/client_secret.py deleted file mode 100644 index 338c1b62..00000000 --- a/src/workos/types/connect/client_secret.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Literal, Optional - -from workos.types.workos_model import WorkOSModel - - -class ClientSecret(WorkOSModel): - object: Literal["connect_application_secret"] - id: str - secret: Optional[str] = None - secret_hint: str - last_used_at: Optional[str] = None - created_at: str - updated_at: str diff --git a/src/workos/types/connect/connect_application.py b/src/workos/types/connect/connect_application.py deleted file mode 100644 index 4f4a5c83..00000000 --- a/src/workos/types/connect/connect_application.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Literal, Optional, Sequence - -from workos.types.workos_model import WorkOSModel -from workos.typing.literals import LiteralOrUntyped - - -ApplicationType = Literal["oauth", "m2m"] - - -class RedirectUri(WorkOSModel): - uri: str - default: Optional[bool] = None - - -class ConnectApplication(WorkOSModel): - object: Literal["connect_application"] - id: str - client_id: str - name: str - description: Optional[str] = None - application_type: LiteralOrUntyped[ApplicationType] - organization_id: Optional[str] = None - scopes: Sequence[str] = [] - created_at: str - updated_at: str - redirect_uris: Optional[Sequence[RedirectUri]] = None - uses_pkce: Optional[bool] = None - is_first_party: Optional[bool] = None - was_dynamically_registered: Optional[bool] = None diff --git a/src/workos/types/connect/list_filters.py b/src/workos/types/connect/list_filters.py deleted file mode 100644 index 56bf5d48..00000000 --- a/src/workos/types/connect/list_filters.py +++ /dev/null @@ -1,7 +0,0 @@ -from typing import Optional - -from workos.types.list_resource import ListArgs - - -class ConnectApplicationListFilters(ListArgs, total=False): - organization_id: Optional[str] diff --git a/src/workos/types/connect/redirect_uri_input.py b/src/workos/types/connect/redirect_uri_input.py deleted file mode 100644 index f900adce..00000000 --- a/src/workos/types/connect/redirect_uri_input.py +++ /dev/null @@ -1,9 +0,0 @@ -from typing_extensions import TypedDict - - -class _RedirectUriInputRequired(TypedDict): - uri: str - - -class RedirectUriInput(_RedirectUriInputRequired, total=False): - default: bool diff --git a/src/workos/types/directory_sync/__init__.py b/src/workos/types/directory_sync/__init__.py deleted file mode 100644 index 3a1d07cd..00000000 --- a/src/workos/types/directory_sync/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .directory_group import * -from .directory_state import * -from .directory_type import * -from .directory_user import * -from .directory import * diff --git a/src/workos/types/directory_sync/directory.py b/src/workos/types/directory_sync/directory.py deleted file mode 100644 index 79703116..00000000 --- a/src/workos/types/directory_sync/directory.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import Literal, Optional -from workos.types.workos_model import WorkOSModel -from workos.types.directory_sync.directory_state import DirectoryState -from workos.types.directory_sync.directory_type import DirectoryType -from workos.typing.literals import LiteralOrUntyped - - -class DirectoryUsersMetadata(WorkOSModel): - active: int - inactive: int - - -class DirectoryMetadata(WorkOSModel): - users: DirectoryUsersMetadata - groups: int - - -class Directory(WorkOSModel): - """Representation of a Directory Response as returned by WorkOS through the Directory Sync feature.""" - - id: str - object: Literal["directory"] - domain: Optional[str] = None - name: str - organization_id: str - external_key: str - state: LiteralOrUntyped[DirectoryState] - type: LiteralOrUntyped[DirectoryType] - metadata: Optional[DirectoryMetadata] = None - created_at: str - updated_at: str diff --git a/src/workos/types/directory_sync/directory_group.py b/src/workos/types/directory_sync/directory_group.py deleted file mode 100644 index 3b4af126..00000000 --- a/src/workos/types/directory_sync/directory_group.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import Any, Literal, Mapping -from workos.types.workos_model import WorkOSModel - - -class DirectoryGroup(WorkOSModel): - """Representation of a Directory Group as returned by WorkOS through the Directory Sync feature.""" - - id: str - object: Literal["directory_group"] - idp_id: str - name: str - directory_id: str - organization_id: str - raw_attributes: Mapping[str, Any] - created_at: str - updated_at: str diff --git a/src/workos/types/directory_sync/directory_state.py b/src/workos/types/directory_sync/directory_state.py deleted file mode 100644 index 4be3c0c0..00000000 --- a/src/workos/types/directory_sync/directory_state.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Any, Literal -from pydantic import BeforeValidator, ValidationInfo -from typing_extensions import Annotated - - -ApiDirectoryState = Literal[ - "active", - "inactive", - "validating", - "deleting", - "invalid_credentials", -] - - -def convert_legacy_directory_state(value: Any, info: ValidationInfo) -> Any: - if isinstance(value, str): - if value == "linked": - return "active" - elif value == "unlinked": - return "inactive" - - return value - - -DirectoryState = Annotated[ - ApiDirectoryState, - BeforeValidator(convert_legacy_directory_state), -] diff --git a/src/workos/types/directory_sync/directory_type.py b/src/workos/types/directory_sync/directory_type.py deleted file mode 100644 index 4fc882dd..00000000 --- a/src/workos/types/directory_sync/directory_type.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import Literal - - -DirectoryType = Literal[ - "azure scim v2.0", - "bamboohr", - "breathe hr", - "cezanne hr", - "cyperark scim v2.0", - "fourth hr", - "generic scim v2.0", - "gsuite directory", - "hibob", - "jump cloud scim v2.0", - "okta scim v2.0", - "onelogin scim v2.0", - "people hr", - "personio", - "pingfederate scim v2.0", - "rippling v2.0", - "sftp", - "sftp workday", - "workday", -] diff --git a/src/workos/types/directory_sync/directory_user.py b/src/workos/types/directory_sync/directory_user.py deleted file mode 100644 index 387948fa..00000000 --- a/src/workos/types/directory_sync/directory_user.py +++ /dev/null @@ -1,50 +0,0 @@ -from typing import Any, Dict, Literal, Optional, Sequence, Union - -from workos.types.workos_model import WorkOSModel -from workos.types.directory_sync.directory_group import DirectoryGroup - - -DirectoryUserState = Literal["active", "inactive"] - - -class DirectoryUserEmail(WorkOSModel): - type: Optional[str] = None - value: Optional[str] = None - primary: Optional[bool] = None - - -class InlineRole(WorkOSModel): - slug: str - - -class DirectoryUser(WorkOSModel): - id: str - object: Literal["directory_user"] - idp_id: str - directory_id: str - organization_id: str - first_name: Optional[str] = None - last_name: Optional[str] = None - email: Optional[str] = None - # @deprecated Will be removed in a future major version. Enable the `job_title` custom attribute in dashboard and pull from customAttributes instead. See https://workos.com/docs/directory-sync/attributes/custom-attributes/auto-mapped-attributes for details. - job_title: Optional[str] = None - # @deprecated Will be removed in a future major version. Enable the `emails` custom attribute in dashboard and pull from customAttributes instead. See https://workos.com/docs/directory-sync/attributes/custom-attributes/auto-mapped-attributes for details. - emails: Sequence[DirectoryUserEmail] - # @deprecated Will be removed in a future major version. Enable the `username` custom attribute in dashboard and pull from customAttributes instead. See https://workos.com/docs/directory-sync/attributes/custom-attributes/auto-mapped-attributes for details. - username: Optional[str] = None - state: DirectoryUserState - custom_attributes: Dict[str, Any] - raw_attributes: Dict[str, Any] - created_at: str - updated_at: str - role: Optional[InlineRole] = None - roles: Optional[Sequence[InlineRole]] = None - - def primary_email(self) -> Union[DirectoryUserEmail, None]: - return next((email for email in self.emails if email.primary), None) - - -class DirectoryUserWithGroups(DirectoryUser): - """Representation of a Directory User as returned by WorkOS through the Directory Sync feature.""" - - groups: Sequence[DirectoryGroup] diff --git a/src/workos/types/directory_sync/list_filters.py b/src/workos/types/directory_sync/list_filters.py deleted file mode 100644 index 01a1e9ba..00000000 --- a/src/workos/types/directory_sync/list_filters.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Optional -from workos.types.list_resource import ListArgs - - -class DirectoryListFilters(ListArgs, total=False): - search: Optional[str] - organization_id: Optional[str] - domain: Optional[str] - - -class DirectoryUserListFilters( - ListArgs, - total=False, -): - group_id: Optional[str] - directory_id: Optional[str] - - -class DirectoryGroupListFilters(ListArgs, total=False): - user_id: Optional[str] - directory_id: Optional[str] diff --git a/src/workos/types/events/__init__.py b/src/workos/types/events/__init__.py deleted file mode 100644 index f403d9a1..00000000 --- a/src/workos/types/events/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -from .authentication_payload import * -from .connection_payload_with_legacy_fields import * -from .connection_saml_certificate_payload import * -from .directory_group_membership_payload import * -from .directory_group_with_previous_attributes import * -from .directory_payload import * -from .directory_payload_with_legacy_fields import * -from .directory_user_with_previous_attributes import * -from .event_model import * -from .event_type import * -from .event import * -from .flag_payload import * -from .organization_domain_verification_failed_payload import * -from .previous_attributes import * -from .session_payload import * -from .vault_payload import * diff --git a/src/workos/types/events/authentication_payload.py b/src/workos/types/events/authentication_payload.py deleted file mode 100644 index 05ebe02e..00000000 --- a/src/workos/types/events/authentication_payload.py +++ /dev/null @@ -1,104 +0,0 @@ -from typing import Literal, Optional -from workos.types.workos_model import WorkOSModel - - -class AuthenticationResultCommon(WorkOSModel): - ip_address: Optional[str] = None - user_agent: Optional[str] = None - - -class AuthenticationResultSucceeded(AuthenticationResultCommon): - status: Literal["succeeded"] - email: str - - -class ErrorWithCode(WorkOSModel): - code: str - message: str - - -class AuthenticationResultFailed(AuthenticationResultCommon): - status: Literal["failed"] - error: ErrorWithCode - email: Optional[str] = None - user_id: Optional[str] = None - - -class AuthenticationEmailVerificationSucceededPayload(AuthenticationResultSucceeded): - type: Literal["email_verification"] - user_id: str - - -class AuthenticationEmailVerificationFailedPayload(AuthenticationResultFailed): - type: Literal["email_verification"] - - -class AuthenticationMagicAuthFailedPayload(AuthenticationResultFailed): - type: Literal["magic_auth"] - - -class AuthenticationMagicAuthSucceededPayload(AuthenticationResultSucceeded): - type: Literal["magic_auth"] - user_id: str - - -class AuthenticationMfaSucceededPayload(AuthenticationResultSucceeded): - type: Literal["mfa"] - user_id: Optional[str] = None - - -class AuthenticationMfaFailedPayload(AuthenticationResultFailed): - type: Literal["mfa"] - - -class AuthenticationOauthFailedPayload(AuthenticationResultFailed): - type: Literal["oauth"] - - -class AuthenticationOauthSucceededPayload(AuthenticationResultSucceeded): - type: Literal["oauth"] - user_id: Optional[str] = None - - -class AuthenticationPasskeyFailedPayload(AuthenticationResultFailed): - type: Literal["passkey"] - - -class AuthenticationPasskeySucceededPayload(AuthenticationResultSucceeded): - type: Literal["passkey"] - user_id: str - - -class AuthenticationPasswordFailedPayload(AuthenticationResultFailed): - type: Literal["password"] - - -class AuthenticationPasswordSucceededPayload(AuthenticationResultSucceeded): - type: Literal["password"] - user_id: str - - -class AuthenticationSsoData(WorkOSModel): - connection_id: Optional[str] = None - organization_id: Optional[str] = None - session_id: Optional[str] = None - - -class AuthenticationSsoFailedPayload(AuthenticationResultFailed): - type: Literal["sso"] - sso: AuthenticationSsoData - - -class AuthenticationSsoSucceededPayload(AuthenticationResultSucceeded): - type: Literal["sso"] - user_id: Optional[str] = None - sso: AuthenticationSsoData - - -class AuthenticationRadarRiskDetectedPayload(AuthenticationResultCommon): - auth_method: str - action: str - control: Optional[str] = None - blocklist_type: Optional[str] = None - user_id: str - email: str diff --git a/src/workos/types/events/connection_payload_with_legacy_fields.py b/src/workos/types/events/connection_payload_with_legacy_fields.py deleted file mode 100644 index bbd23410..00000000 --- a/src/workos/types/events/connection_payload_with_legacy_fields.py +++ /dev/null @@ -1,5 +0,0 @@ -from workos.types.sso import ConnectionWithDomains - - -class ConnectionPayloadWithLegacyFields(ConnectionWithDomains): - external_key: str diff --git a/src/workos/types/events/connection_saml_certificate_payload.py b/src/workos/types/events/connection_saml_certificate_payload.py deleted file mode 100644 index 784aca1d..00000000 --- a/src/workos/types/events/connection_saml_certificate_payload.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import Literal, Optional -from workos.types.workos_model import WorkOSModel - -SamlCertificateType = Literal["ResponseSigning", "RequestSigning", "ResponseEncryption"] - - -class SamlCertificateConnection(WorkOSModel): - id: str - organization_id: Optional[str] = None - - -class SamlCertificate(WorkOSModel): - certificate_type: SamlCertificateType - expiry_date: str - - -class SamlCertificateWithExpiry(SamlCertificate): - is_expired: bool - - -class ConnectionSamlCertificateRenewedPayload(WorkOSModel): - connection: SamlCertificateConnection - certificate: SamlCertificate - renewed_at: str - - -class ConnectionSamlCertificateRenewalRequiredPayload(WorkOSModel): - connection: SamlCertificateConnection - certificate: SamlCertificateWithExpiry - days_until_expiry: int diff --git a/src/workos/types/events/directory_group_membership_payload.py b/src/workos/types/events/directory_group_membership_payload.py deleted file mode 100644 index e49aab6d..00000000 --- a/src/workos/types/events/directory_group_membership_payload.py +++ /dev/null @@ -1,9 +0,0 @@ -from workos.types.directory_sync import DirectoryGroup -from workos.types.workos_model import WorkOSModel -from workos.types.directory_sync.directory_user import DirectoryUser - - -class DirectoryGroupMembershipPayload(WorkOSModel): - directory_id: str - user: DirectoryUser - group: DirectoryGroup diff --git a/src/workos/types/events/directory_group_with_previous_attributes.py b/src/workos/types/events/directory_group_with_previous_attributes.py deleted file mode 100644 index c34cb8aa..00000000 --- a/src/workos/types/events/directory_group_with_previous_attributes.py +++ /dev/null @@ -1,6 +0,0 @@ -from workos.types.directory_sync import DirectoryGroup -from workos.types.events.previous_attributes import PreviousAttributes - - -class DirectoryGroupWithPreviousAttributes(DirectoryGroup): - previous_attributes: PreviousAttributes diff --git a/src/workos/types/events/directory_payload.py b/src/workos/types/events/directory_payload.py deleted file mode 100644 index fd1137ff..00000000 --- a/src/workos/types/events/directory_payload.py +++ /dev/null @@ -1,16 +0,0 @@ -from typing import Literal -from workos.types.directory_sync import DirectoryType -from workos.types.workos_model import WorkOSModel -from workos.types.directory_sync.directory_state import DirectoryState -from workos.typing.literals import LiteralOrUntyped - - -class DirectoryPayload(WorkOSModel): - id: str - name: str - state: LiteralOrUntyped[DirectoryState] - type: LiteralOrUntyped[DirectoryType] - organization_id: str - created_at: str - updated_at: str - object: Literal["directory"] diff --git a/src/workos/types/events/directory_payload_with_legacy_fields.py b/src/workos/types/events/directory_payload_with_legacy_fields.py deleted file mode 100644 index 630ff76b..00000000 --- a/src/workos/types/events/directory_payload_with_legacy_fields.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Literal, Sequence -from workos.types.workos_model import WorkOSModel -from workos.types.events.directory_payload import DirectoryPayload - - -class MinimalOrganizationDomain(WorkOSModel): - id: str - # TODO: This should be domain: str in the - # next major version to fix object parsing. - organization_id: str - object: Literal["organization_domain"] - - -# TODO: This class should be removed in the next major version once MinimalOrganizationDomain is updated. -class MinimalOrganizationDomainForEventsApi(WorkOSModel): - id: str - domain: str - object: Literal["organization_domain"] - - -class DirectoryPayloadWithLegacyFields(DirectoryPayload): - domains: Sequence[MinimalOrganizationDomain] - external_key: str - - -# TODO: This class should be removed in the next major version once MinimalOrganizationDomain is updated. -class DirectoryPayloadWithLegacyFieldsForEventsApi(DirectoryPayload): - domains: Sequence[MinimalOrganizationDomainForEventsApi] - external_key: str diff --git a/src/workos/types/events/directory_user_with_previous_attributes.py b/src/workos/types/events/directory_user_with_previous_attributes.py deleted file mode 100644 index a87ba931..00000000 --- a/src/workos/types/events/directory_user_with_previous_attributes.py +++ /dev/null @@ -1,6 +0,0 @@ -from workos.types.directory_sync.directory_user import DirectoryUser -from workos.types.events.previous_attributes import PreviousAttributes - - -class DirectoryUserWithPreviousAttributes(DirectoryUser): - previous_attributes: PreviousAttributes diff --git a/src/workos/types/events/event.py b/src/workos/types/events/event.py deleted file mode 100644 index 8b72c967..00000000 --- a/src/workos/types/events/event.py +++ /dev/null @@ -1,504 +0,0 @@ -from typing import Literal, Union -from pydantic import Field -from typing_extensions import Annotated -from workos.types.user_management import OrganizationMembership, User -from workos.types.directory_sync.directory_group import DirectoryGroup -from workos.types.directory_sync.directory_user import DirectoryUser -from workos.types.api_keys import ApiKey -from workos.types.events.authentication_payload import ( - AuthenticationEmailVerificationFailedPayload, - AuthenticationEmailVerificationSucceededPayload, - AuthenticationMagicAuthFailedPayload, - AuthenticationMagicAuthSucceededPayload, - AuthenticationMfaFailedPayload, - AuthenticationMfaSucceededPayload, - AuthenticationOauthFailedPayload, - AuthenticationOauthSucceededPayload, - AuthenticationPasskeyFailedPayload, - AuthenticationPasskeySucceededPayload, - AuthenticationPasswordFailedPayload, - AuthenticationPasswordSucceededPayload, - AuthenticationRadarRiskDetectedPayload, - AuthenticationSsoFailedPayload, - AuthenticationSsoSucceededPayload, -) -from workos.types.events.connection_payload_with_legacy_fields import ( - ConnectionPayloadWithLegacyFields, -) -from workos.types.events.connection_saml_certificate_payload import ( - ConnectionSamlCertificateRenewedPayload, - ConnectionSamlCertificateRenewalRequiredPayload, -) -from workos.types.events.directory_group_membership_payload import ( - DirectoryGroupMembershipPayload, -) -from workos.types.events.directory_group_with_previous_attributes import ( - DirectoryGroupWithPreviousAttributes, -) -from workos.types.events.directory_payload import DirectoryPayload -from workos.types.events.directory_payload_with_legacy_fields import ( - DirectoryPayloadWithLegacyFieldsForEventsApi, -) -from workos.types.events.directory_user_with_previous_attributes import ( - DirectoryUserWithPreviousAttributes, -) -from workos.types.authorization.organization_role import OrganizationRoleEvent -from workos.types.authorization.permission import Permission -from workos.types.events.event_model import EventModel -from workos.types.events.flag_payload import FlagPayload, FlagRuleUpdatedContext -from workos.types.events.organization_domain_verification_failed_payload import ( - OrganizationDomainVerificationFailedPayload, -) -from workos.types.events.session_payload import ( - SessionCreatedPayload, - SessionRevokedPayload, -) -from workos.types.events.vault_payload import ( - VaultDataCreatedPayload, - VaultDataDeletedPayload, - VaultDataReadPayload, - VaultDataUpdatedPayload, - VaultDekDecryptedPayload, - VaultDekReadPayload, - VaultKekCreatedPayload, - VaultMetadataReadPayload, - VaultNamesListedPayload, -) -from workos.types.organizations.organization_common import OrganizationCommon -from workos.types.organization_domains import OrganizationDomain -from workos.types.roles.role import EventRole -from workos.types.sso.connection import Connection -from workos.types.user_management.email_verification import ( - EmailVerificationCommon, -) -from workos.types.user_management.invitation import InvitationCommon -from workos.types.user_management.magic_auth import MagicAuthCommon -from workos.types.user_management.password_reset import PasswordResetCommon - - -# README -# When adding a new event type, ensure the new event class is -# added to the Event union type at the bottom of this file, and -# the event name is added to the EventType union type in event_type.py. - - -class ApiKeyCreatedEvent(EventModel[ApiKey]): - event: Literal["api_key.created"] - - -class ApiKeyRevokedEvent(EventModel[ApiKey]): - event: Literal["api_key.revoked"] - - -class AuthenticationEmailVerificationFailedEvent( - EventModel[AuthenticationEmailVerificationFailedPayload,] -): - event: Literal["authentication.email_verification_failed"] - - -class AuthenticationEmailVerificationSucceededEvent( - EventModel[AuthenticationEmailVerificationSucceededPayload,] -): - event: Literal["authentication.email_verification_succeeded"] - - -class AuthenticationMagicAuthFailedEvent( - EventModel[AuthenticationMagicAuthFailedPayload,] -): - event: Literal["authentication.magic_auth_failed"] - - -class AuthenticationMagicAuthSucceededEvent( - EventModel[AuthenticationMagicAuthSucceededPayload,] -): - event: Literal["authentication.magic_auth_succeeded"] - - -class AuthenticationMfaFailedEvent(EventModel[AuthenticationMfaFailedPayload]): - event: Literal["authentication.mfa_failed"] - - -class AuthenticationMfaSucceededEvent(EventModel[AuthenticationMfaSucceededPayload]): - event: Literal["authentication.mfa_succeeded"] - - -class AuthenticationOauthFailedEvent(EventModel[AuthenticationOauthFailedPayload]): - event: Literal["authentication.oauth_failed"] - - -class AuthenticationOauthSucceededEvent( - EventModel[AuthenticationOauthSucceededPayload] -): - event: Literal["authentication.oauth_succeeded"] - - -class AuthenticationPasskeyFailedEvent(EventModel[AuthenticationPasskeyFailedPayload]): - event: Literal["authentication.passkey_failed"] - - -class AuthenticationPasskeySucceededEvent( - EventModel[AuthenticationPasskeySucceededPayload] -): - event: Literal["authentication.passkey_succeeded"] - - -class AuthenticationPasswordFailedEvent( - EventModel[AuthenticationPasswordFailedPayload] -): - event: Literal["authentication.password_failed"] - - -class AuthenticationPasswordSucceededEvent( - EventModel[AuthenticationPasswordSucceededPayload,] -): - event: Literal["authentication.password_succeeded"] - - -class AuthenticationRadarRiskDetectedEvent( - EventModel[AuthenticationRadarRiskDetectedPayload] -): - event: Literal["authentication.radar_risk_detected"] - - -class AuthenticationSsoFailedEvent(EventModel[AuthenticationSsoFailedPayload]): - event: Literal["authentication.sso_failed"] - - -class AuthenticationSsoSucceededEvent(EventModel[AuthenticationSsoSucceededPayload]): - event: Literal["authentication.sso_succeeded"] - - -class ConnectionActivatedEvent(EventModel[ConnectionPayloadWithLegacyFields]): - event: Literal["connection.activated"] - - -class ConnectionDeactivatedEvent(EventModel[ConnectionPayloadWithLegacyFields]): - event: Literal["connection.deactivated"] - - -class ConnectionDeletedEvent(EventModel[Connection]): - event: Literal["connection.deleted"] - - -class ConnectionSamlCertificateRenewedEvent( - EventModel[ConnectionSamlCertificateRenewedPayload] -): - event: Literal["connection.saml_certificate_renewed"] - - -class ConnectionSamlCertificateRenewalRequiredEvent( - EventModel[ConnectionSamlCertificateRenewalRequiredPayload] -): - event: Literal["connection.saml_certificate_renewal_required"] - - -class DirectoryActivatedEvent(EventModel[DirectoryPayloadWithLegacyFieldsForEventsApi]): - event: Literal["dsync.activated"] - - -class DirectoryDeletedEvent(EventModel[DirectoryPayload]): - event: Literal["dsync.deleted"] - - -class DirectoryGroupCreatedEvent(EventModel[DirectoryGroup]): - event: Literal["dsync.group.created"] - - -class DirectoryGroupDeletedEvent(EventModel[DirectoryGroup]): - event: Literal["dsync.group.deleted"] - - -class DirectoryGroupUpdatedEvent(EventModel[DirectoryGroupWithPreviousAttributes]): - event: Literal["dsync.group.updated"] - - -class DirectoryUserCreatedEvent(EventModel[DirectoryUser]): - event: Literal["dsync.user.created"] - - -class DirectoryUserDeletedEvent(EventModel[DirectoryUser]): - event: Literal["dsync.user.deleted"] - - -class DirectoryUserUpdatedEvent(EventModel[DirectoryUserWithPreviousAttributes]): - event: Literal["dsync.user.updated"] - - -class DirectoryUserAddedToGroupEvent(EventModel[DirectoryGroupMembershipPayload]): - event: Literal["dsync.group.user_added"] - - -class DirectoryUserRemovedFromGroupEvent(EventModel[DirectoryGroupMembershipPayload]): - event: Literal["dsync.group.user_removed"] - - -class EmailVerificationCreatedEvent(EventModel[EmailVerificationCommon]): - event: Literal["email_verification.created"] - - -class FlagCreatedEvent(EventModel[FlagPayload]): - event: Literal["flag.created"] - - -class FlagDeletedEvent(EventModel[FlagPayload]): - event: Literal["flag.deleted"] - - -class FlagRuleUpdatedEvent(EventModel[FlagPayload]): - event: Literal["flag.rule_updated"] - context: FlagRuleUpdatedContext - - -class FlagUpdatedEvent(EventModel[FlagPayload]): - event: Literal["flag.updated"] - - -class InvitationAcceptedEvent(EventModel[InvitationCommon]): - event: Literal["invitation.accepted"] - - -class InvitationCreatedEvent(EventModel[InvitationCommon]): - event: Literal["invitation.created"] - - -class InvitationResentEvent(EventModel[InvitationCommon]): - event: Literal["invitation.resent"] - - -class InvitationRevokedEvent(EventModel[InvitationCommon]): - event: Literal["invitation.revoked"] - - -class MagicAuthCreatedEvent(EventModel[MagicAuthCommon]): - event: Literal["magic_auth.created"] - - -class OrganizationCreatedEvent(EventModel[OrganizationCommon]): - event: Literal["organization.created"] - - -class OrganizationDeletedEvent(EventModel[OrganizationCommon]): - event: Literal["organization.deleted"] - - -class OrganizationUpdatedEvent(EventModel[OrganizationCommon]): - event: Literal["organization.updated"] - - -class OrganizationDomainVerificationFailedEvent( - EventModel[OrganizationDomainVerificationFailedPayload,] -): - event: Literal["organization_domain.verification_failed"] - - -class OrganizationDomainVerifiedEvent(EventModel[OrganizationDomain]): - event: Literal["organization_domain.verified"] - - -class OrganizationDomainCreatedEvent(EventModel[OrganizationDomain]): - event: Literal["organization_domain.created"] - - -class OrganizationDomainUpdatedEvent(EventModel[OrganizationDomain]): - event: Literal["organization_domain.updated"] - - -class OrganizationDomainDeletedEvent(EventModel[OrganizationDomain]): - event: Literal["organization_domain.deleted"] - - -class OrganizationMembershipCreatedEvent(EventModel[OrganizationMembership]): - event: Literal["organization_membership.created"] - - -class OrganizationMembershipDeletedEvent(EventModel[OrganizationMembership]): - event: Literal["organization_membership.deleted"] - - -class OrganizationMembershipUpdatedEvent(EventModel[OrganizationMembership]): - event: Literal["organization_membership.updated"] - - -class OrganizationRoleCreatedEvent(EventModel[OrganizationRoleEvent]): - event: Literal["organization_role.created"] - - -class OrganizationRoleUpdatedEvent(EventModel[OrganizationRoleEvent]): - event: Literal["organization_role.updated"] - - -class OrganizationRoleDeletedEvent(EventModel[OrganizationRoleEvent]): - event: Literal["organization_role.deleted"] - - -class PasswordResetCreatedEvent(EventModel[PasswordResetCommon]): - event: Literal["password_reset.created"] - - -class PasswordResetSucceededEvent(EventModel[PasswordResetCommon]): - event: Literal["password_reset.succeeded"] - - -class PermissionCreatedEvent(EventModel[Permission]): - event: Literal["permission.created"] - - -class PermissionUpdatedEvent(EventModel[Permission]): - event: Literal["permission.updated"] - - -class PermissionDeletedEvent(EventModel[Permission]): - event: Literal["permission.deleted"] - - -class RoleCreatedEvent(EventModel[EventRole]): - event: Literal["role.created"] - - -class RoleDeletedEvent(EventModel[EventRole]): - event: Literal["role.deleted"] - - -class RoleUpdatedEvent(EventModel[EventRole]): - event: Literal["role.updated"] - - -class SessionCreatedEvent(EventModel[SessionCreatedPayload]): - event: Literal["session.created"] - - -class SessionRevokedEvent(EventModel[SessionRevokedPayload]): - event: Literal["session.revoked"] - - -class UserCreatedEvent(EventModel[User]): - event: Literal["user.created"] - - -class UserDeletedEvent(EventModel[User]): - event: Literal["user.deleted"] - - -class UserUpdatedEvent(EventModel[User]): - event: Literal["user.updated"] - - -class VaultDataCreatedEvent(EventModel[VaultDataCreatedPayload]): - event: Literal["vault.data.created"] - - -class VaultDataDeletedEvent(EventModel[VaultDataDeletedPayload]): - event: Literal["vault.data.deleted"] - - -class VaultDataReadEvent(EventModel[VaultDataReadPayload]): - event: Literal["vault.data.read"] - - -class VaultDataUpdatedEvent(EventModel[VaultDataUpdatedPayload]): - event: Literal["vault.data.updated"] - - -class VaultDekDecryptedEvent(EventModel[VaultDekDecryptedPayload]): - event: Literal["vault.dek.decrypted"] - - -class VaultDekReadEvent(EventModel[VaultDekReadPayload]): - event: Literal["vault.dek.read"] - - -class VaultKekCreatedEvent(EventModel[VaultKekCreatedPayload]): - event: Literal["vault.kek.created"] - - -class VaultMetadataReadEvent(EventModel[VaultMetadataReadPayload]): - event: Literal["vault.metadata.read"] - - -class VaultNamesListedEvent(EventModel[VaultNamesListedPayload]): - event: Literal["vault.names.listed"] - - -Event = Annotated[ - Union[ - ApiKeyCreatedEvent, - ApiKeyRevokedEvent, - AuthenticationEmailVerificationFailedEvent, - AuthenticationEmailVerificationSucceededEvent, - AuthenticationMagicAuthFailedEvent, - AuthenticationMagicAuthSucceededEvent, - AuthenticationMfaFailedEvent, - AuthenticationMfaSucceededEvent, - AuthenticationOauthFailedEvent, - AuthenticationOauthSucceededEvent, - AuthenticationPasskeyFailedEvent, - AuthenticationPasskeySucceededEvent, - AuthenticationPasswordFailedEvent, - AuthenticationPasswordSucceededEvent, - AuthenticationRadarRiskDetectedEvent, - AuthenticationSsoFailedEvent, - AuthenticationSsoSucceededEvent, - ConnectionActivatedEvent, - ConnectionDeactivatedEvent, - ConnectionDeletedEvent, - ConnectionSamlCertificateRenewedEvent, - ConnectionSamlCertificateRenewalRequiredEvent, - DirectoryActivatedEvent, - DirectoryDeletedEvent, - DirectoryGroupCreatedEvent, - DirectoryGroupDeletedEvent, - DirectoryGroupUpdatedEvent, - DirectoryUserCreatedEvent, - DirectoryUserDeletedEvent, - DirectoryUserUpdatedEvent, - DirectoryUserAddedToGroupEvent, - DirectoryUserRemovedFromGroupEvent, - EmailVerificationCreatedEvent, - FlagCreatedEvent, - FlagDeletedEvent, - FlagRuleUpdatedEvent, - FlagUpdatedEvent, - InvitationAcceptedEvent, - InvitationCreatedEvent, - InvitationResentEvent, - InvitationRevokedEvent, - MagicAuthCreatedEvent, - OrganizationCreatedEvent, - OrganizationDeletedEvent, - OrganizationUpdatedEvent, - OrganizationDomainCreatedEvent, - OrganizationDomainDeletedEvent, - OrganizationDomainUpdatedEvent, - OrganizationDomainVerificationFailedEvent, - OrganizationDomainVerifiedEvent, - OrganizationMembershipCreatedEvent, - OrganizationMembershipDeletedEvent, - OrganizationMembershipUpdatedEvent, - OrganizationRoleCreatedEvent, - OrganizationRoleUpdatedEvent, - OrganizationRoleDeletedEvent, - PasswordResetCreatedEvent, - PasswordResetSucceededEvent, - PermissionCreatedEvent, - PermissionUpdatedEvent, - PermissionDeletedEvent, - RoleCreatedEvent, - RoleDeletedEvent, - RoleUpdatedEvent, - SessionCreatedEvent, - SessionRevokedEvent, - UserCreatedEvent, - UserDeletedEvent, - UserUpdatedEvent, - VaultDataCreatedEvent, - VaultDataDeletedEvent, - VaultDataReadEvent, - VaultDataUpdatedEvent, - VaultDekDecryptedEvent, - VaultDekReadEvent, - VaultKekCreatedEvent, - VaultMetadataReadEvent, - VaultNamesListedEvent, - ], - Field(..., discriminator="event"), -] diff --git a/src/workos/types/events/event_model.py b/src/workos/types/events/event_model.py deleted file mode 100644 index b87ee36f..00000000 --- a/src/workos/types/events/event_model.py +++ /dev/null @@ -1,146 +0,0 @@ -from typing import Generic, Literal, TypeVar -from workos.types.user_management import OrganizationMembership, User -from workos.types.workos_model import WorkOSModel -from workos.types.directory_sync.directory_group import DirectoryGroup -from workos.types.directory_sync.directory_user import DirectoryUser -from workos.types.api_keys import ApiKey -from workos.types.events.authentication_payload import ( - AuthenticationEmailVerificationFailedPayload, - AuthenticationEmailVerificationSucceededPayload, - AuthenticationMagicAuthFailedPayload, - AuthenticationMagicAuthSucceededPayload, - AuthenticationMfaFailedPayload, - AuthenticationMfaSucceededPayload, - AuthenticationOauthFailedPayload, - AuthenticationOauthSucceededPayload, - AuthenticationPasskeyFailedPayload, - AuthenticationPasskeySucceededPayload, - AuthenticationPasswordFailedPayload, - AuthenticationPasswordSucceededPayload, - AuthenticationRadarRiskDetectedPayload, - AuthenticationSsoFailedPayload, - AuthenticationSsoSucceededPayload, -) -from workos.types.events.connection_payload_with_legacy_fields import ( - ConnectionPayloadWithLegacyFields, -) -from workos.types.events.connection_saml_certificate_payload import ( - ConnectionSamlCertificateRenewedPayload, - ConnectionSamlCertificateRenewalRequiredPayload, -) -from workos.types.events.directory_group_membership_payload import ( - DirectoryGroupMembershipPayload, -) -from workos.types.events.directory_group_with_previous_attributes import ( - DirectoryGroupWithPreviousAttributes, -) -from workos.types.events.directory_payload import DirectoryPayload -from workos.types.events.directory_payload_with_legacy_fields import ( - DirectoryPayloadWithLegacyFields, - DirectoryPayloadWithLegacyFieldsForEventsApi, -) -from workos.types.events.directory_user_with_previous_attributes import ( - DirectoryUserWithPreviousAttributes, -) -from workos.types.events.flag_payload import FlagPayload -from workos.types.events.organization_domain_verification_failed_payload import ( - OrganizationDomainVerificationFailedPayload, -) -from workos.types.events.session_payload import ( - SessionCreatedPayload, - SessionRevokedPayload, -) -from workos.types.events.vault_payload import ( - VaultDataCreatedPayload, - VaultDataDeletedPayload, - VaultDataReadPayload, - VaultDataUpdatedPayload, - VaultDekDecryptedPayload, - VaultDekReadPayload, - VaultKekCreatedPayload, - VaultMetadataReadPayload, - VaultNamesListedPayload, -) -from workos.types.organizations.organization_common import OrganizationCommon -from workos.types.organization_domains import OrganizationDomain -from workos.types.authorization.organization_role import OrganizationRoleEvent -from workos.types.authorization.permission import Permission -from workos.types.roles.role import EventRole -from workos.types.sso.connection import Connection -from workos.types.user_management.email_verification import ( - EmailVerificationCommon, -) -from workos.types.user_management.invitation import InvitationCommon -from workos.types.user_management.magic_auth import MagicAuthCommon -from workos.types.user_management.password_reset import PasswordResetCommon - - -EventPayload = TypeVar( - "EventPayload", - ApiKey, - AuthenticationEmailVerificationFailedPayload, - AuthenticationEmailVerificationSucceededPayload, - AuthenticationMagicAuthFailedPayload, - AuthenticationMagicAuthSucceededPayload, - AuthenticationMfaFailedPayload, - AuthenticationMfaSucceededPayload, - AuthenticationOauthFailedPayload, - AuthenticationOauthSucceededPayload, - AuthenticationPasskeyFailedPayload, - AuthenticationPasskeySucceededPayload, - AuthenticationPasswordFailedPayload, - AuthenticationPasswordSucceededPayload, - AuthenticationRadarRiskDetectedPayload, - AuthenticationSsoFailedPayload, - AuthenticationSsoSucceededPayload, - Connection, - ConnectionPayloadWithLegacyFields, - ConnectionSamlCertificateRenewedPayload, - ConnectionSamlCertificateRenewalRequiredPayload, - DirectoryPayload, - DirectoryPayloadWithLegacyFields, - # TODO: Remove once merged with DirectoryPayloadWithLegacyFields in next major release. - DirectoryPayloadWithLegacyFieldsForEventsApi, - DirectoryGroup, - DirectoryGroupWithPreviousAttributes, - DirectoryUser, - DirectoryUserWithPreviousAttributes, - DirectoryGroupMembershipPayload, - EmailVerificationCommon, - EventRole, - FlagPayload, - InvitationCommon, - MagicAuthCommon, - OrganizationCommon, - OrganizationDomain, - OrganizationDomainVerificationFailedPayload, - OrganizationMembership, - OrganizationRoleEvent, - PasswordResetCommon, - Permission, - SessionCreatedPayload, - SessionRevokedPayload, - User, - VaultDataCreatedPayload, - VaultDataDeletedPayload, - VaultDataReadPayload, - VaultDataUpdatedPayload, - VaultDekDecryptedPayload, - VaultDekReadPayload, - VaultKekCreatedPayload, - VaultMetadataReadPayload, - VaultNamesListedPayload, -) - - -class EventModel(WorkOSModel, Generic[EventPayload]): - # TODO: fix these docs - """Representation of an Event returned from the Events API or via Webhook. - Attributes: - OBJECT_FIELDS (list): List of fields an Event is comprised of. - """ - - id: str - object: Literal["event"] - data: EventPayload - created_at: str diff --git a/src/workos/types/events/event_type.py b/src/workos/types/events/event_type.py deleted file mode 100644 index b657158f..00000000 --- a/src/workos/types/events/event_type.py +++ /dev/null @@ -1,88 +0,0 @@ -from typing import Literal, TypeVar - -# README -# When adding a new event type, ensure a new event class is created -# and added to the Event class union type in event.py. - -EventType = Literal[ - "api_key.created", - "api_key.revoked", - "authentication.email_verification_failed", - "authentication.email_verification_succeeded", - "authentication.magic_auth_failed", - "authentication.magic_auth_succeeded", - "authentication.mfa_failed", - "authentication.mfa_succeeded", - "authentication.oauth_failed", - "authentication.oauth_succeeded", - "authentication.passkey_failed", - "authentication.passkey_succeeded", - "authentication.password_failed", - "authentication.password_succeeded", - "authentication.radar_risk_detected", - "authentication.sso_failed", - "authentication.sso_succeeded", - "connection.activated", - "connection.deactivated", - "connection.deleted", - "connection.saml_certificate_renewed", - "connection.saml_certificate_renewal_required", - "dsync.activated", - "dsync.deleted", - "dsync.group.created", - "dsync.group.deleted", - "dsync.group.updated", - "dsync.user.created", - "dsync.user.deleted", - "dsync.user.updated", - "dsync.group.user_added", - "dsync.group.user_removed", - "email_verification.created", - "flag.created", - "flag.deleted", - "flag.rule_updated", - "flag.updated", - "invitation.accepted", - "invitation.created", - "invitation.resent", - "invitation.revoked", - "magic_auth.created", - "organization.created", - "organization.deleted", - "organization.updated", - "organization_domain.verification_failed", - "organization_domain.verified", - "organization_domain.created", - "organization_domain.deleted", - "organization_domain.updated", - "organization_membership.created", - "organization_membership.deleted", - "organization_membership.updated", - "organization_role.created", - "organization_role.updated", - "organization_role.deleted", - "password_reset.created", - "password_reset.succeeded", - "permission.created", - "permission.updated", - "permission.deleted", - "role.created", - "role.deleted", - "role.updated", - "session.created", - "session.revoked", - "user.created", - "user.deleted", - "user.updated", - "vault.data.created", - "vault.data.deleted", - "vault.data.read", - "vault.data.updated", - "vault.dek.decrypted", - "vault.dek.read", - "vault.kek.created", - "vault.metadata.read", - "vault.names.listed", -] - -EventTypeDiscriminator = TypeVar("EventTypeDiscriminator", bound=EventType) diff --git a/src/workos/types/events/flag_payload.py b/src/workos/types/events/flag_payload.py deleted file mode 100644 index b382990f..00000000 --- a/src/workos/types/events/flag_payload.py +++ /dev/null @@ -1,70 +0,0 @@ -from typing import Literal, Optional, Sequence - -from workos.types.workos_model import WorkOSModel - -AccessType = Literal["none", "some", "all"] - - -class FlagOwner(WorkOSModel): - email: str - first_name: Optional[str] = None - last_name: Optional[str] = None - - -class FlagPayload(WorkOSModel): - object: Literal["feature_flag"] - id: str - environment_id: str - slug: str - name: str - description: Optional[str] = None - owner: Optional[FlagOwner] = None - tags: Sequence[str] - enabled: bool - default_value: bool - created_at: str - updated_at: str - - -class FlagRuleActor(WorkOSModel): - id: str - source: Literal["api", "dashboard", "system"] - name: Optional[str] = None - - -class FlagRuleOrganizationTarget(WorkOSModel): - id: str - name: str - - -class FlagRuleUserTarget(WorkOSModel): - id: str - email: str - - -class FlagRuleConfiguredTargets(WorkOSModel): - organizations: Sequence[FlagRuleOrganizationTarget] - users: Sequence[FlagRuleUserTarget] - - -class FlagRulePreviousDataAttributes(WorkOSModel): - enabled: Optional[bool] = None - default_value: Optional[bool] = None - - -class FlagRulePreviousContextAttributes(WorkOSModel): - access_type: Optional[AccessType] = None - configured_targets: Optional[FlagRuleConfiguredTargets] = None - - -class FlagRulePreviousAttributes(WorkOSModel): - data: Optional[FlagRulePreviousDataAttributes] = None - context: Optional[FlagRulePreviousContextAttributes] = None - - -class FlagRuleUpdatedContext(WorkOSModel): - client_id: str - actor: FlagRuleActor - access_type: AccessType - configured_targets: FlagRuleConfiguredTargets - previous_attributes: FlagRulePreviousAttributes diff --git a/src/workos/types/events/list_filters.py b/src/workos/types/events/list_filters.py deleted file mode 100644 index ab4d920c..00000000 --- a/src/workos/types/events/list_filters.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Optional, Sequence -from workos.types.events import EventType -from workos.types.list_resource import ListArgs - - -class EventsListFilters(ListArgs, total=False): - events: Sequence[EventType] - organization_id: Optional[str] - range_start: Optional[str] - range_end: Optional[str] diff --git a/src/workos/types/events/organization_domain_verification_failed_payload.py b/src/workos/types/events/organization_domain_verification_failed_payload.py deleted file mode 100644 index 1df3a061..00000000 --- a/src/workos/types/events/organization_domain_verification_failed_payload.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import Literal -from workos.types.workos_model import WorkOSModel -from workos.types.organization_domains import OrganizationDomain -from workos.typing.literals import LiteralOrUntyped - - -class OrganizationDomainVerificationFailedPayload(WorkOSModel): - reason: LiteralOrUntyped[ - Literal[ - "domain_verification_period_expired", - "domain_verified_by_other_organization", - ] - ] - organization_domain: OrganizationDomain diff --git a/src/workos/types/events/previous_attributes.py b/src/workos/types/events/previous_attributes.py deleted file mode 100644 index dfcb52b3..00000000 --- a/src/workos/types/events/previous_attributes.py +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Any, Mapping - -PreviousAttributes = Mapping[str, Any] diff --git a/src/workos/types/events/session_payload.py b/src/workos/types/events/session_payload.py deleted file mode 100644 index 7ab9a079..00000000 --- a/src/workos/types/events/session_payload.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import Literal, Optional -from workos.types.workos_model import WorkOSModel -from workos.types.user_management.impersonator import Impersonator - - -class SessionCreatedPayload(WorkOSModel): - object: Literal["session"] - id: str - impersonator: Optional[Impersonator] = None - ip_address: Optional[str] = None - organization_id: Optional[str] = None - user_agent: Optional[str] = None - user_id: str - created_at: str - updated_at: str - - -class SessionRevokedPayload(WorkOSModel): - object: Literal["session"] - id: str - impersonator: Optional[Impersonator] = None - ip_address: Optional[str] = None - organization_id: Optional[str] = None - user_agent: Optional[str] = None - user_id: str - created_at: str - updated_at: str diff --git a/src/workos/types/events/vault_payload.py b/src/workos/types/events/vault_payload.py deleted file mode 100644 index 5534e4cc..00000000 --- a/src/workos/types/events/vault_payload.py +++ /dev/null @@ -1,65 +0,0 @@ -from typing import List, Optional - -from workos.types.vault.key import KeyContext -from workos.types.workos_model import WorkOSModel - - -class VaultNamesListedPayload(WorkOSModel): - actor_id: str - actor_source: str - actor_name: str - - -class VaultDataDeletedPayload(WorkOSModel): - actor_id: str - actor_source: str - actor_name: str - kv_name: str - - -class VaultDekDecryptedPayload(WorkOSModel): - actor_id: str - actor_source: str - actor_name: str - key_id: str - - -class VaultDataReadPayload(WorkOSModel): - actor_id: str - actor_source: str - actor_name: str - kv_name: str - key_id: str - - -class VaultDataCreatedPayload(WorkOSModel): - actor_id: str - actor_source: str - actor_name: str - kv_name: str - key_id: str - key_context: Optional[KeyContext] = None - - -class VaultDekReadPayload(WorkOSModel): - actor_id: str - actor_source: str - actor_name: str - key_ids: List[str] - key_context: Optional[KeyContext] = None - - -class VaultKekCreatedPayload(WorkOSModel): - actor_id: str - actor_source: str - actor_name: str - key_name: str - key_id: str - - -class VaultDataUpdatedPayload(VaultDataCreatedPayload): - pass - - -class VaultMetadataReadPayload(VaultDataDeletedPayload): - pass diff --git a/src/workos/types/feature_flags/__init__.py b/src/workos/types/feature_flags/__init__.py deleted file mode 100644 index 0547a748..00000000 --- a/src/workos/types/feature_flags/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from workos.types.feature_flags.feature_flag import FeatureFlag - -__all__ = ["FeatureFlag"] diff --git a/src/workos/types/feature_flags/feature_flag.py b/src/workos/types/feature_flags/feature_flag.py deleted file mode 100644 index b634539f..00000000 --- a/src/workos/types/feature_flags/feature_flag.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Literal, Optional -from workos.types.workos_model import WorkOSModel - - -class FeatureFlag(WorkOSModel): - id: str - object: Literal["feature_flag"] - slug: str - name: str - description: Optional[str] - created_at: str - updated_at: str diff --git a/src/workos/types/feature_flags/list_filters.py b/src/workos/types/feature_flags/list_filters.py deleted file mode 100644 index 965d455c..00000000 --- a/src/workos/types/feature_flags/list_filters.py +++ /dev/null @@ -1,5 +0,0 @@ -from workos.types.list_resource import ListArgs - - -class FeatureFlagListFilters(ListArgs, total=False): - pass diff --git a/src/workos/types/fga/__init__.py b/src/workos/types/fga/__init__.py deleted file mode 100644 index 7d8a640c..00000000 --- a/src/workos/types/fga/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .check import * -from .authorization_resource_types import * -from .authorization_resources import * -from .warrant import * -from .warnings import * diff --git a/src/workos/types/fga/authorization_resource_types.py b/src/workos/types/fga/authorization_resource_types.py deleted file mode 100644 index ccfcfb81..00000000 --- a/src/workos/types/fga/authorization_resource_types.py +++ /dev/null @@ -1,9 +0,0 @@ -from typing import Any, Mapping, Optional - -from workos.types.workos_model import WorkOSModel - - -class AuthorizationResourceType(WorkOSModel): - type: str - relations: Mapping[str, Any] - created_at: Optional[str] = None diff --git a/src/workos/types/fga/authorization_resources.py b/src/workos/types/fga/authorization_resources.py deleted file mode 100644 index 921bc87f..00000000 --- a/src/workos/types/fga/authorization_resources.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Any, Mapping, Optional - -from workos.types.workos_model import WorkOSModel - - -class AuthorizationResource(WorkOSModel): - resource_type: str - resource_id: str - meta: Optional[Mapping[str, Any]] = None - created_at: Optional[str] = None diff --git a/src/workos/types/fga/check.py b/src/workos/types/fga/check.py deleted file mode 100644 index cac38520..00000000 --- a/src/workos/types/fga/check.py +++ /dev/null @@ -1,51 +0,0 @@ -from typing import Any, Literal, Mapping, Optional, Sequence, TypedDict - -from workos.types.workos_model import WorkOSModel -from workos.typing.literals import LiteralOrUntyped - -from .warnings import FGAWarning -from .warrant import Subject, SubjectInput - -CheckOperation = Literal["any_of", "all_of", "batch"] - - -class WarrantCheckInput(TypedDict, total=False): - resource_type: str - resource_id: str - relation: str - subject: SubjectInput - context: Optional[Mapping[str, Any]] - - -class WarrantCheck(WorkOSModel): - resource_type: str - resource_id: str - relation: str - subject: Subject - context: Optional[Mapping[str, Any]] = None - - -class DecisionTreeNode(WorkOSModel): - check: WarrantCheck - decision: str - processing_time: int - children: Optional[Sequence["DecisionTreeNode"]] = None - policy: Optional[str] = None - - -class DebugInfo(WorkOSModel): - processing_time: int - decision_tree: DecisionTreeNode - - -CheckResult = Literal["authorized", "not_authorized"] - - -class CheckResponse(WorkOSModel): - result: LiteralOrUntyped[CheckResult] - is_implicit: bool - debug_info: Optional[DebugInfo] = None - warnings: Optional[Sequence[FGAWarning]] = None - - def authorized(self) -> bool: - return self.result == "authorized" diff --git a/src/workos/types/fga/list_filters.py b/src/workos/types/fga/list_filters.py deleted file mode 100644 index 7fa6dde5..00000000 --- a/src/workos/types/fga/list_filters.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import Mapping, Optional, Any - -from workos.types.list_resource import ListArgs - - -class AuthorizationResourceListFilters(ListArgs, total=False): - resource_type: Optional[str] - search: Optional[str] - - -class WarrantListFilters(ListArgs, total=False): - resource_type: Optional[str] - resource_id: Optional[str] - relation: Optional[str] - subject_type: Optional[str] - subject_id: Optional[str] - subject_relation: Optional[str] - warrant_token: Optional[str] - - -class WarrantQueryListFilters(ListArgs, total=False): - q: Optional[str] - context: Optional[Mapping[str, Any]] - warrant_token: Optional[str] diff --git a/src/workos/types/fga/warnings.py b/src/workos/types/fga/warnings.py deleted file mode 100644 index 40b7c8ed..00000000 --- a/src/workos/types/fga/warnings.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import Any, Dict, Literal, Sequence, Union - -from pydantic import BeforeValidator -from pydantic_core.core_schema import ValidationInfo -from typing_extensions import Annotated - -from workos.types.workos_model import WorkOSModel - - -class FGABaseWarning(WorkOSModel): - code: str - message: str - - -class MissingContextKeysWarning(FGABaseWarning): # type: ignore[override, unused-ignore] - code: Literal["missing_context_keys"] - keys: Sequence[str] - - -def fga_warning_dispatch_validator( - value: Dict[str, Any], info: ValidationInfo -) -> FGABaseWarning: - if value.get("code") == "missing_context_keys": - return MissingContextKeysWarning.model_validate(value) - - # Fallback to the base warning model - return FGABaseWarning.model_validate(value) - - -FGAWarning = Annotated[ - Union[MissingContextKeysWarning, FGABaseWarning], - BeforeValidator(fga_warning_dispatch_validator), -] diff --git a/src/workos/types/fga/warrant.py b/src/workos/types/fga/warrant.py deleted file mode 100644 index 2e1b55a3..00000000 --- a/src/workos/types/fga/warrant.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import Literal, Mapping, Optional, Any -from typing_extensions import TypedDict - -from workos.types.workos_model import WorkOSModel - - -class SubjectInput(TypedDict, total=False): - resource_type: str - resource_id: str - relation: Optional[str] - - -class Subject(WorkOSModel): - resource_type: str - resource_id: str - relation: Optional[str] = None - - -class Warrant(WorkOSModel): - resource_type: str - resource_id: str - relation: str - subject: Subject - policy: Optional[str] = None - - -class WriteWarrantResponse(WorkOSModel): - warrant_token: str - - -WarrantWriteOperation = Literal["create", "delete"] - - -class WarrantWrite(TypedDict, total=False): - op: WarrantWriteOperation - resource_type: str - resource_id: str - relation: str - subject: SubjectInput - policy: Optional[str] - - -class WarrantQueryResult(WorkOSModel): - resource_type: str - resource_id: str - relation: str - warrant: Warrant - is_implicit: bool - meta: Optional[Mapping[str, Any]] = None diff --git a/src/workos/types/list_resource.py b/src/workos/types/list_resource.py deleted file mode 100644 index c1eb22f3..00000000 --- a/src/workos/types/list_resource.py +++ /dev/null @@ -1,216 +0,0 @@ -from pydantic import BaseModel, Field -from typing import ( - Any, - Awaitable, - AsyncIterator, - Dict, - Literal, - Mapping, - Sequence, - Tuple, - TypeVar, - Generic, - Callable, - Iterator, - Optional, - Union, - cast, -) -from typing_extensions import Required, TypedDict -from workos.types.api_keys import ApiKey -from workos.types.audit_logs import AuditLogAction, AuditLogSchema -from workos.types.authorization.organization_membership import ( - AuthorizationOrganizationMembership, -) -from workos.types.authorization.permission import Permission -from workos.types.authorization.authorization_resource import AuthorizationResource -from workos.types.authorization.role_assignment import RoleAssignment -from workos.types.connect import ClientSecret, ConnectApplication -from workos.types.directory_sync import ( - Directory, - DirectoryGroup, - DirectoryUserWithGroups, -) -from workos.types.events import Event -from workos.types.feature_flags import FeatureFlag -from workos.types.fga import ( - AuthorizationResource as FGAAuthorizationResource, - AuthorizationResourceType, - Warrant, - WarrantQueryResult, -) -from workos.types.mfa import AuthenticationFactor -from workos.types.organizations import Organization -from workos.types.sso import ConnectionWithDomains -from workos.types.user_management import Invitation, OrganizationMembership, User -from workos.types.user_management.session import Session as UserManagementSession -from workos.types.vault import ObjectDigest -from workos.types.workos_model import WorkOSModel -from workos.utils.request_helper import DEFAULT_LIST_RESPONSE_LIMIT - -ListableResource = TypeVar( - # add all possible generics of List Resource - "ListableResource", - ApiKey, - AuditLogAction, - AuditLogSchema, - AuthenticationFactor, - ClientSecret, - ConnectApplication, - ConnectionWithDomains, - Directory, - DirectoryGroup, - DirectoryUserWithGroups, - Event, - FeatureFlag, - Invitation, - Organization, - OrganizationMembership, - Permission, - AuthorizationResource, - RoleAssignment, - AuthorizationOrganizationMembership, - FGAAuthorizationResource, - AuthorizationResourceType, - User, - UserManagementSession, - ObjectDigest, - Warrant, - WarrantQueryResult, -) - - -class ListAfterMetadata(BaseModel): - after: Optional[str] = None - - -class ListMetadata(ListAfterMetadata): - before: Optional[str] = None - - -ListMetadataType = TypeVar("ListMetadataType", ListAfterMetadata, ListMetadata) - - -class ListPage(WorkOSModel, Generic[ListableResource]): - object: Literal["list"] - data: Sequence[ListableResource] - list_metadata: ListMetadata - - -class ListArgs(TypedDict, total=False): - before: Optional[str] - after: Optional[str] - limit: Required[int] - order: Optional[Literal["asc", "desc"]] - - -ListAndFilterParams = TypeVar("ListAndFilterParams", bound=ListArgs) - - -class WorkOSListResource( - WorkOSModel, - Generic[ListableResource, ListAndFilterParams, ListMetadataType], -): - object: Literal["list"] - data: Sequence[ListableResource] - list_metadata: ListMetadataType - - # TODO: Fix type hinting for list_method to support both sync and async - list_method: Union[ - Callable[ - ..., - "WorkOSListResource[ListableResource, ListAndFilterParams, ListMetadataType]", - ], - Callable[ - ..., - "Awaitable[WorkOSListResource[ListableResource, ListAndFilterParams, ListMetadataType]]", - ], - ] = Field(exclude=True) - list_args: ListAndFilterParams = Field(exclude=True) - - def _parse_params( - self, limit_override: Optional[int] = None - ) -> Tuple[Dict[str, Union[int, str, None]], Mapping[str, Any]]: - fixed_pagination_params = cast( - # Type hints consider this a mismatch because it assume the dictionary is dict[str, int] - Dict[str, Union[int, str, None]], - { - "limit": limit_override or self.list_args["limit"], - }, - ) - if "order" in self.list_args: - fixed_pagination_params["order"] = self.list_args["order"] - - # Omit common list parameters - filter_params = { - k: v - for k, v in self.list_args.items() - if k not in {"order", "limit", "before", "after"} - } - - return fixed_pagination_params, filter_params - - # Pydantic uses a custom `__iter__` method to support casting BaseModels - # to dictionaries. e.g. dict(model). - # As we want to support `for item in page`, this is inherently incompatible - # with the default pydantic behaviour. It is not possible to support both - # use cases at once. Fortunately, this is not a big deal as all other pydantic - # methods should continue to work as expected as there is an alternative method - # to cast a model to a dictionary, model.dict(), which is used internally - # by pydantic. - def __iter__(self) -> Iterator[ListableResource]: # type: ignore - next_page: WorkOSListResource[ - ListableResource, ListAndFilterParams, ListMetadataType - ] - after = self.list_metadata.after - fixed_pagination_params, filter_params = self._parse_params( - # Singe we're auto-paginating, ignore the original limit and use the default - limit_override=DEFAULT_LIST_RESPONSE_LIMIT - ) - index: int = 0 - - while True: - if index >= len(self.data): - if after is not None: - # TODO: Fix type hinting for list_method to support both sync and async - # We use a union to support both sync and async methods, - # but when we get to the particular implementation, it - # doesn't know which one it is. It's safe, but should be fixed. - next_page = self.list_method( - after=after, **fixed_pagination_params, **filter_params - ) # type: ignore - self.data = next_page.data - after = next_page.list_metadata.after - index = 0 - continue - else: - return - yield self.data[index] - index += 1 - - async def __aiter__(self) -> AsyncIterator[ListableResource]: - next_page: WorkOSListResource[ - ListableResource, ListAndFilterParams, ListMetadataType - ] - after = self.list_metadata.after - fixed_pagination_params, filter_params = self._parse_params() - index: int = 0 - - while True: - if index >= len(self.data): - if after is not None: - # TODO: Fix type hinting for list_method to support both sync and async - # We use a union to support both sync and async methods, - # but when we get to the particular implementation, it - # doesn't know which one it is. It's safe, but should be fixed. - next_page = await self.list_method( - after=after, **fixed_pagination_params, **filter_params - ) # type: ignore - self.data = next_page.data - after = next_page.list_metadata.after - index = 0 - continue - else: - return - yield self.data[index] - index += 1 diff --git a/src/workos/types/metadata.py b/src/workos/types/metadata.py deleted file mode 100644 index 9a108d10..00000000 --- a/src/workos/types/metadata.py +++ /dev/null @@ -1,4 +0,0 @@ -from typing import Dict - - -Metadata = Dict[str, str] diff --git a/src/workos/types/mfa/__init__.py b/src/workos/types/mfa/__init__.py deleted file mode 100644 index be9b674b..00000000 --- a/src/workos/types/mfa/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .authentication_challenge_verification_response import * -from .authentication_challenge import * -from .authentication_factor_totp_and_challenge_response import * -from .authentication_factor import * -from .enroll_authentication_factor_type import * diff --git a/src/workos/types/mfa/authentication_challenge.py b/src/workos/types/mfa/authentication_challenge.py deleted file mode 100644 index de0100a5..00000000 --- a/src/workos/types/mfa/authentication_challenge.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import Literal, Optional -from workos.types.workos_model import WorkOSModel - - -class AuthenticationChallenge(WorkOSModel): - """Representation of a MFA Challenge Response as returned by WorkOS through the MFA feature.""" - - object: Literal["authentication_challenge"] - id: str - created_at: str - updated_at: str - expires_at: Optional[str] = None - code: Optional[str] = None - authentication_factor_id: str diff --git a/src/workos/types/mfa/authentication_challenge_verification_response.py b/src/workos/types/mfa/authentication_challenge_verification_response.py deleted file mode 100644 index 096ed0dc..00000000 --- a/src/workos/types/mfa/authentication_challenge_verification_response.py +++ /dev/null @@ -1,9 +0,0 @@ -from workos.types.workos_model import WorkOSModel -from workos.types.mfa.authentication_challenge import AuthenticationChallenge - - -class AuthenticationChallengeVerificationResponse(WorkOSModel): - """Representation of a WorkOS MFA Challenge Verification Response.""" - - challenge: AuthenticationChallenge - valid: bool diff --git a/src/workos/types/mfa/authentication_factor.py b/src/workos/types/mfa/authentication_factor.py deleted file mode 100644 index a6f89761..00000000 --- a/src/workos/types/mfa/authentication_factor.py +++ /dev/null @@ -1,69 +0,0 @@ -from typing import Literal, Optional, Union - -from workos.types.mfa.enroll_authentication_factor_type import ( - SmsAuthenticationFactorType, - TotpAuthenticationFactorType, -) -from workos.types.workos_model import WorkOSModel -from workos.typing.literals import LiteralOrUntyped - -AuthenticationFactorType = Literal[ - "generic_otp", SmsAuthenticationFactorType, TotpAuthenticationFactorType -] - - -class TotpFactor(WorkOSModel): - """Representation of a TOTP factor when returned in list resources and sessions.""" - - issuer: str - user: str - - -class ExtendedTotpFactor(TotpFactor): - """Representation of a TOTP factor when returned when enrolling an authentication factor.""" - - qr_code: str - secret: str - uri: str - - -class SmsFactor(WorkOSModel): - phone_number: str - - -class AuthenticationFactorBase(WorkOSModel): - """Representation of a MFA Authentication Factor Response as returned by WorkOS through the MFA feature.""" - - object: Literal["authentication_factor"] - id: str - created_at: str - updated_at: str - type: LiteralOrUntyped[AuthenticationFactorType] - user_id: Optional[str] = None - - -class AuthenticationFactorTotp(AuthenticationFactorBase): # type: ignore[override, unused-ignore] - """Representation of a MFA Authentication Factor Response as returned by WorkOS through the MFA feature.""" - - type: TotpAuthenticationFactorType - totp: TotpFactor - - -class AuthenticationFactorTotpExtended(AuthenticationFactorBase): # type: ignore[override, unused-ignore] - """Representation of a MFA Authentication Factor Response when enrolling an authentication factor.""" - - type: TotpAuthenticationFactorType - totp: ExtendedTotpFactor - - -class AuthenticationFactorSms(AuthenticationFactorBase): # type: ignore[override, unused-ignore] - """Representation of a SMS Authentication Factor Response as returned by WorkOS through the MFA feature.""" - - type: SmsAuthenticationFactorType - sms: SmsFactor - - -AuthenticationFactor = Union[AuthenticationFactorTotp, AuthenticationFactorSms] -AuthenticationFactorExtended = Union[ - AuthenticationFactorTotpExtended, AuthenticationFactorSms -] diff --git a/src/workos/types/mfa/authentication_factor_totp_and_challenge_response.py b/src/workos/types/mfa/authentication_factor_totp_and_challenge_response.py deleted file mode 100644 index 56404b81..00000000 --- a/src/workos/types/mfa/authentication_factor_totp_and_challenge_response.py +++ /dev/null @@ -1,10 +0,0 @@ -from workos.types.workos_model import WorkOSModel -from workos.types.mfa.authentication_challenge import AuthenticationChallenge -from workos.types.mfa.authentication_factor import AuthenticationFactorTotpExtended - - -class AuthenticationFactorTotpAndChallengeResponse(WorkOSModel): - """Representation of an authentication factor and authentication challenge response as returned by WorkOS through User Management features.""" - - authentication_factor: AuthenticationFactorTotpExtended - authentication_challenge: AuthenticationChallenge diff --git a/src/workos/types/mfa/enroll_authentication_factor_type.py b/src/workos/types/mfa/enroll_authentication_factor_type.py deleted file mode 100644 index 157eab1a..00000000 --- a/src/workos/types/mfa/enroll_authentication_factor_type.py +++ /dev/null @@ -1,8 +0,0 @@ -from typing import Literal - -SmsAuthenticationFactorType = Literal["sms"] -TotpAuthenticationFactorType = Literal["totp"] - -EnrollAuthenticationFactorType = Literal[ - SmsAuthenticationFactorType, TotpAuthenticationFactorType -] diff --git a/src/workos/types/organization_domains/__init__.py b/src/workos/types/organization_domains/__init__.py deleted file mode 100644 index efdc7d2d..00000000 --- a/src/workos/types/organization_domains/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .organization_domain import * diff --git a/src/workos/types/organization_domains/organization_domain.py b/src/workos/types/organization_domains/organization_domain.py deleted file mode 100644 index 3a6dafcd..00000000 --- a/src/workos/types/organization_domains/organization_domain.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Literal, Optional -from workos.types.workos_model import WorkOSModel -from workos.typing.literals import LiteralOrUntyped - - -class OrganizationDomain(WorkOSModel): - id: str - organization_id: str - object: Literal["organization_domain"] - domain: str - state: Optional[ - LiteralOrUntyped[Literal["failed", "pending", "legacy_verified", "verified"]] - ] = None - verification_strategy: Optional[LiteralOrUntyped[Literal["manual", "dns"]]] = None - verification_token: Optional[str] = None - verification_prefix: Optional[str] = None - created_at: str - updated_at: str diff --git a/src/workos/types/organizations/__init__.py b/src/workos/types/organizations/__init__.py deleted file mode 100644 index 71655a2e..00000000 --- a/src/workos/types/organizations/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from .domain_data_input import * -from .organization_common import * -from .organization import * - -# re-exported for backwards compatibility, can be removed after version 6.0.0 -from workos.types.organization_domains import OrganizationDomain diff --git a/src/workos/types/organizations/domain_data_input.py b/src/workos/types/organizations/domain_data_input.py deleted file mode 100644 index 0aeb0070..00000000 --- a/src/workos/types/organizations/domain_data_input.py +++ /dev/null @@ -1,7 +0,0 @@ -from typing import Literal -from typing_extensions import TypedDict - - -class DomainDataInput(TypedDict): - domain: str - state: Literal["verified", "pending"] diff --git a/src/workos/types/organizations/list_filters.py b/src/workos/types/organizations/list_filters.py deleted file mode 100644 index f3e7d867..00000000 --- a/src/workos/types/organizations/list_filters.py +++ /dev/null @@ -1,6 +0,0 @@ -from typing import Optional, Sequence -from workos.types.list_resource import ListArgs - - -class OrganizationListFilters(ListArgs, total=False): - domains: Optional[Sequence[str]] diff --git a/src/workos/types/organizations/organization.py b/src/workos/types/organizations/organization.py deleted file mode 100644 index e6c35922..00000000 --- a/src/workos/types/organizations/organization.py +++ /dev/null @@ -1,12 +0,0 @@ -from dataclasses import field -from typing import Optional, Sequence -from workos.types.metadata import Metadata -from workos.types.organizations.organization_common import OrganizationCommon -from workos.types.organization_domains import OrganizationDomain - - -class Organization(OrganizationCommon): - allow_profiles_outside_organization: bool - domains: Sequence[OrganizationDomain] - stripe_customer_id: Optional[str] = None - metadata: Metadata = field(default_factory=dict) diff --git a/src/workos/types/organizations/organization_common.py b/src/workos/types/organizations/organization_common.py deleted file mode 100644 index b3ab61e2..00000000 --- a/src/workos/types/organizations/organization_common.py +++ /dev/null @@ -1,13 +0,0 @@ -from typing import Literal, Optional, Sequence -from workos.types.workos_model import WorkOSModel -from workos.types.organization_domains import OrganizationDomain - - -class OrganizationCommon(WorkOSModel): - id: str - object: Literal["organization"] - name: str - external_id: Optional[str] = None - domains: Sequence[OrganizationDomain] - created_at: str - updated_at: str diff --git a/src/workos/types/passwordless/__init__.py b/src/workos/types/passwordless/__init__.py deleted file mode 100644 index b70e0876..00000000 --- a/src/workos/types/passwordless/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .passwordless_session_type import * -from .passwordless_session import * diff --git a/src/workos/types/passwordless/passwordless_session.py b/src/workos/types/passwordless/passwordless_session.py deleted file mode 100644 index d7cff3d3..00000000 --- a/src/workos/types/passwordless/passwordless_session.py +++ /dev/null @@ -1,12 +0,0 @@ -from typing import Literal -from workos.types.workos_model import WorkOSModel - - -class PasswordlessSession(WorkOSModel): - """Representation of a WorkOS Passwordless Session Response.""" - - object: Literal["passwordless_session"] - id: str - email: str - expires_at: str - link: str diff --git a/src/workos/types/passwordless/passwordless_session_type.py b/src/workos/types/passwordless/passwordless_session_type.py deleted file mode 100644 index af3db6f6..00000000 --- a/src/workos/types/passwordless/passwordless_session_type.py +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Literal - -PasswordlessSessionType = Literal["MagicLink"] diff --git a/src/workos/types/pipes/__init__.py b/src/workos/types/pipes/__init__.py deleted file mode 100644 index 27e630dd..00000000 --- a/src/workos/types/pipes/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from workos.types.pipes.pipes import ( - AccessToken as AccessToken, - GetAccessTokenFailureResponse as GetAccessTokenFailureResponse, - GetAccessTokenResponse as GetAccessTokenResponse, - GetAccessTokenSuccessResponse as GetAccessTokenSuccessResponse, -) diff --git a/src/workos/types/pipes/pipes.py b/src/workos/types/pipes/pipes.py deleted file mode 100644 index 6d93db85..00000000 --- a/src/workos/types/pipes/pipes.py +++ /dev/null @@ -1,34 +0,0 @@ -from datetime import datetime -from typing import Literal, Optional, Sequence, Union - -from workos.types.workos_model import WorkOSModel - - -class AccessToken(WorkOSModel): - """Represents an OAuth access token for a third-party provider.""" - - object: Literal["access_token"] - access_token: str - expires_at: Optional[datetime] = None - scopes: Sequence[str] - missing_scopes: Sequence[str] - - -class GetAccessTokenSuccessResponse(WorkOSModel): - """Successful response containing the access token.""" - - active: Literal[True] - access_token: AccessToken - - -class GetAccessTokenFailureResponse(WorkOSModel): - """Failed response indicating why the token couldn't be retrieved.""" - - active: Literal[False] - error: Literal["not_installed", "needs_reauthorization"] - - -GetAccessTokenResponse = Union[ - GetAccessTokenSuccessResponse, - GetAccessTokenFailureResponse, -] diff --git a/src/workos/types/portal/__init__.py b/src/workos/types/portal/__init__.py deleted file mode 100644 index 6437b5e3..00000000 --- a/src/workos/types/portal/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .portal_link_intent import * -from .portal_link import * diff --git a/src/workos/types/portal/portal_link.py b/src/workos/types/portal/portal_link.py deleted file mode 100644 index f5663fb6..00000000 --- a/src/workos/types/portal/portal_link.py +++ /dev/null @@ -1,7 +0,0 @@ -from workos.types.workos_model import WorkOSModel - - -class PortalLink(WorkOSModel): - """Representation of an WorkOS generate portal link response.""" - - link: str diff --git a/src/workos/types/portal/portal_link_intent.py b/src/workos/types/portal/portal_link_intent.py deleted file mode 100644 index 3302dfc5..00000000 --- a/src/workos/types/portal/portal_link_intent.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Literal - - -PortalLinkIntent = Literal[ - "audit_logs", - "certificate_renewal", - "domain_verification", - "dsync", - "log_streams", - "sso", -] diff --git a/src/workos/types/portal/portal_link_intent_options.py b/src/workos/types/portal/portal_link_intent_options.py deleted file mode 100644 index 6c634033..00000000 --- a/src/workos/types/portal/portal_link_intent_options.py +++ /dev/null @@ -1,9 +0,0 @@ -from typing import TypedDict - - -class SsoIntentOptions(TypedDict): - bookmark_slug: str - - -class IntentOptions(TypedDict): - sso: SsoIntentOptions diff --git a/src/workos/types/roles/__init__.py b/src/workos/types/roles/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/workos/types/roles/role.py b/src/workos/types/roles/role.py deleted file mode 100644 index d65b9b62..00000000 --- a/src/workos/types/roles/role.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Literal, Optional, Sequence -from workos.types.workos_model import WorkOSModel - -RoleType = Literal["EnvironmentRole", "OrganizationRole"] - - -class RoleCommon(WorkOSModel): - object: Literal["role"] - slug: str - - -class EventRole(RoleCommon): - permissions: Optional[Sequence[str]] = None - created_at: Optional[str] = None - updated_at: Optional[str] = None - - -class Role(RoleCommon): - id: str - name: str - description: Optional[str] = None - type: RoleType - created_at: str - updated_at: str - - -class RoleList(WorkOSModel): - object: Literal["list"] - data: Sequence[Role] diff --git a/src/workos/types/sso/__init__.py b/src/workos/types/sso/__init__.py deleted file mode 100644 index fed914cf..00000000 --- a/src/workos/types/sso/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .connection_domain import * -from .connection import * -from .profile import * -from .sso_provider_type import * diff --git a/src/workos/types/sso/connection.py b/src/workos/types/sso/connection.py deleted file mode 100644 index ca10908f..00000000 --- a/src/workos/types/sso/connection.py +++ /dev/null @@ -1,70 +0,0 @@ -from typing import Literal, Sequence, Optional -from workos.types.sso.connection_domain import ConnectionDomain -from workos.types.workos_model import WorkOSModel -from workos.typing.literals import LiteralOrUntyped - -ConnectionState = Literal[ - "active", "deleting", "inactive", "requires_type", "validating" -] - -ConnectionType = Literal[ - "ADFSSAML", - "AdpOidc", - "AppleOAuth", - "Auth0SAML", - "AzureSAML", - "CasSAML", - "CloudflareSAML", - "ClassLinkSAML", - "CyberArkSAML", - "DuoSAML", - "GenericOIDC", - "GenericSAML", - "GitHubOAuth", - "GoogleOAuth", - "GoogleSAML", - "JumpCloudSAML", - "KeycloakSAML", - "LastPassSAML", - "LoginGovOidc", - "MagicLink", - "MicrosoftOAuth", - "MiniOrangeSAML", - "NetIqSAML", - "OktaSAML", - "OneLoginSAML", - "OracleSAML", - "PingFederateSAML", - "PingOneSAML", - "RipplingSAML", - "SalesforceOAuth", - "SalesforceSAML", - "ShibbolethGenericSAML", - "ShibbolethSAML", - "SimpleSamlPhpSAML", - "VMwareSAML", -] - - -class SamlConnectionOptions(WorkOSModel): - """Representation of options payload of a Connection Response.""" - - signing_cert: Optional[str] - - -class Connection(WorkOSModel): - object: Literal["connection"] - id: str - organization_id: Optional[str] = None - connection_type: LiteralOrUntyped[ConnectionType] - name: str - state: LiteralOrUntyped[ConnectionState] - created_at: str - updated_at: str - options: Optional[SamlConnectionOptions] = None - - -class ConnectionWithDomains(Connection): - """Representation of a Connection Response as returned by WorkOS through the SSO feature.""" - - domains: Sequence[ConnectionDomain] diff --git a/src/workos/types/sso/connection_domain.py b/src/workos/types/sso/connection_domain.py deleted file mode 100644 index 0abf57fb..00000000 --- a/src/workos/types/sso/connection_domain.py +++ /dev/null @@ -1,8 +0,0 @@ -from typing import Literal -from workos.types.workos_model import WorkOSModel - - -class ConnectionDomain(WorkOSModel): - object: Literal["connection_domain"] - id: str - domain: str diff --git a/src/workos/types/sso/profile.py b/src/workos/types/sso/profile.py deleted file mode 100644 index af53641e..00000000 --- a/src/workos/types/sso/profile.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Any, Literal, Mapping, Optional, Sequence -from workos.types.sso.connection import ConnectionType -from workos.types.workos_model import WorkOSModel -from workos.typing.literals import LiteralOrUntyped -from typing_extensions import TypedDict - - -class ProfileRole(TypedDict): - slug: str - - -class Profile(WorkOSModel): - """Representation of a User Profile as returned by WorkOS through the SSO feature.""" - - object: Literal["profile"] - id: str - connection_id: str - connection_type: LiteralOrUntyped[ConnectionType] - organization_id: Optional[str] = None - email: str - first_name: Optional[str] = None - last_name: Optional[str] = None - idp_id: str - role: Optional[ProfileRole] = None - roles: Optional[Sequence[ProfileRole]] = None - groups: Optional[Sequence[str]] = None - custom_attributes: Optional[Mapping[str, Any]] = None - raw_attributes: Optional[Mapping[str, Any]] = None - - -class ProfileAndToken(WorkOSModel): - """Representation of a User Profile and Access Token as returned by WorkOS through the SSO feature.""" - - access_token: str - profile: Profile diff --git a/src/workos/types/sso/sso_provider_type.py b/src/workos/types/sso/sso_provider_type.py deleted file mode 100644 index aba5d45d..00000000 --- a/src/workos/types/sso/sso_provider_type.py +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Literal - - -SsoProviderType = Literal[ - "AppleOAuth", - "GitHubOAuth", - "GoogleOAuth", - "MicrosoftOAuth", - "SalesforceOAuth", -] diff --git a/src/workos/types/user_management/__init__.py b/src/workos/types/user_management/__init__.py deleted file mode 100644 index aa7cd5c3..00000000 --- a/src/workos/types/user_management/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .authenticate_with_common import * -from .authentication_response import * -from .email_verification import * -from .impersonator import * -from .invitation import * -from .magic_auth import * -from .organization_membership import * -from .password_hash_type import * -from .password_reset import * -from .session import * -from .user import * -from .user_management_provider_type import * diff --git a/src/workos/types/user_management/authenticate_with_common.py b/src/workos/types/user_management/authenticate_with_common.py deleted file mode 100644 index 6e77743f..00000000 --- a/src/workos/types/user_management/authenticate_with_common.py +++ /dev/null @@ -1,66 +0,0 @@ -from typing import Literal, Union -from typing_extensions import TypedDict -from workos.types.user_management.session import SessionConfig - - -class AuthenticateWithBaseParameters(TypedDict): - ip_address: Union[str, None] - user_agent: Union[str, None] - - -class AuthenticateWithPasswordParameters(AuthenticateWithBaseParameters): - email: str - password: str - grant_type: Literal["password"] - - -class AuthenticateWithCodeParameters(AuthenticateWithBaseParameters): - code: str - code_verifier: Union[str, None] - grant_type: Literal["authorization_code"] - session: Union[SessionConfig, None] - invitation_token: Union[str, None] - - -class AuthenticateWithMagicAuthParameters(AuthenticateWithBaseParameters): - code: str - email: str - link_authorization_code: Union[str, None] - grant_type: Literal["urn:workos:oauth:grant-type:magic-auth:code"] - - -class AuthenticateWithEmailVerificationParameters(AuthenticateWithBaseParameters): - code: str - pending_authentication_token: str - grant_type: Literal["urn:workos:oauth:grant-type:email-verification:code"] - - -class AuthenticateWithTotpParameters(AuthenticateWithBaseParameters): - code: str - authentication_challenge_id: str - pending_authentication_token: str - grant_type: Literal["urn:workos:oauth:grant-type:mfa-totp"] - - -class AuthenticateWithOrganizationSelectionParameters(AuthenticateWithBaseParameters): - organization_id: str - pending_authentication_token: str - grant_type: Literal["urn:workos:oauth:grant-type:organization-selection"] - - -class AuthenticateWithRefreshTokenParameters(AuthenticateWithBaseParameters): - refresh_token: str - organization_id: Union[str, None] - grant_type: Literal["refresh_token"] - session: Union[SessionConfig, None] - - -AuthenticateWithParameters = Union[ - AuthenticateWithPasswordParameters, - AuthenticateWithCodeParameters, - AuthenticateWithMagicAuthParameters, - AuthenticateWithEmailVerificationParameters, - AuthenticateWithTotpParameters, - AuthenticateWithOrganizationSelectionParameters, - AuthenticateWithRefreshTokenParameters, -] diff --git a/src/workos/types/user_management/authentication_response.py b/src/workos/types/user_management/authentication_response.py deleted file mode 100644 index ab3ce848..00000000 --- a/src/workos/types/user_management/authentication_response.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import Literal, Optional, TypeVar -from workos.types.user_management.impersonator import Impersonator -from workos.types.user_management.oauth_tokens import OAuthTokens -from workos.types.user_management.user import User -from workos.types.workos_model import WorkOSModel - - -AuthenticationMethod = Literal[ - "SSO", - "Password", - "Passkey", - "AppleOAuth", - "GitHubOAuth", - "GoogleOAuth", - "MicrosoftOAuth", - "SalesforceOAuth", - "MagicAuth", - "Impersonation", -] - - -class _AuthenticationResponseBase(WorkOSModel): - access_token: str - refresh_token: str - - -class AuthenticationResponse(_AuthenticationResponseBase): - """Representation of a WorkOS User and Organization ID response.""" - - authentication_method: Optional[AuthenticationMethod] = None - impersonator: Optional[Impersonator] = None - organization_id: Optional[str] = None - user: User - sealed_session: Optional[str] = None - - -class AuthKitAuthenticationResponse(AuthenticationResponse): - """Representation of a WorkOS User and Organization ID response.""" - - impersonator: Optional[Impersonator] = None - oauth_tokens: Optional[OAuthTokens] = None - - -class RefreshTokenAuthenticationResponse(AuthenticationResponse): - """Representation of a WorkOS refresh token authentication response.""" - - pass - - -AuthenticationResponseType = TypeVar( - "AuthenticationResponseType", - bound=_AuthenticationResponseBase, -) diff --git a/src/workos/types/user_management/email_verification.py b/src/workos/types/user_management/email_verification.py deleted file mode 100644 index 612ce332..00000000 --- a/src/workos/types/user_management/email_verification.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Literal -from workos.types.workos_model import WorkOSModel - - -class EmailVerificationCommon(WorkOSModel): - object: Literal["email_verification"] - id: str - user_id: str - email: str - expires_at: str - created_at: str - updated_at: str - - -class EmailVerification(EmailVerificationCommon): - """Representation of a WorkOS EmailVerification object.""" - - code: str diff --git a/src/workos/types/user_management/impersonator.py b/src/workos/types/user_management/impersonator.py deleted file mode 100644 index b94790c7..00000000 --- a/src/workos/types/user_management/impersonator.py +++ /dev/null @@ -1,8 +0,0 @@ -from workos.types.workos_model import WorkOSModel - - -class Impersonator(WorkOSModel): - """Representation of a WorkOS Dashboard member impersonating a user""" - - email: str - reason: str diff --git a/src/workos/types/user_management/invitation.py b/src/workos/types/user_management/invitation.py deleted file mode 100644 index 5610e0f5..00000000 --- a/src/workos/types/user_management/invitation.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import Literal, Optional -from workos.types.workos_model import WorkOSModel -from workos.typing.literals import LiteralOrUntyped - -InvitationState = Literal["accepted", "expired", "pending", "revoked"] - - -class InvitationCommon(WorkOSModel): - object: Literal["invitation"] - id: str - email: str - state: LiteralOrUntyped[InvitationState] - accepted_at: Optional[str] = None - revoked_at: Optional[str] = None - expires_at: str - organization_id: Optional[str] = None - inviter_user_id: Optional[str] = None - accepted_user_id: Optional[str] = None - created_at: str - updated_at: str - - -class Invitation(InvitationCommon): - """Representation of a WorkOS Invitation as returned.""" - - token: str - accept_invitation_url: str diff --git a/src/workos/types/user_management/list_filters.py b/src/workos/types/user_management/list_filters.py deleted file mode 100644 index a3be45ce..00000000 --- a/src/workos/types/user_management/list_filters.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import Optional, Sequence -from workos.types.list_resource import ListArgs -from workos.types.user_management.organization_membership import ( - OrganizationMembershipStatus, -) - - -class UsersListFilters(ListArgs, total=False): - email: Optional[str] - organization_id: Optional[str] - - -class InvitationsListFilters(ListArgs, total=False): - email: Optional[str] - organization_id: Optional[str] - - -class OrganizationMembershipsListFilters(ListArgs, total=False): - user_id: Optional[str] - organization_id: Optional[str] - statuses: Optional[Sequence[OrganizationMembershipStatus]] - - -class AuthenticationFactorsListFilters(ListArgs, total=False): - user_id: str - - -class SessionsListFilters(ListArgs, total=False): - user_id: str diff --git a/src/workos/types/user_management/magic_auth.py b/src/workos/types/user_management/magic_auth.py deleted file mode 100644 index 2a853142..00000000 --- a/src/workos/types/user_management/magic_auth.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Literal -from workos.types.workos_model import WorkOSModel - - -class MagicAuthCommon(WorkOSModel): - object: Literal["magic_auth"] - id: str - user_id: str - email: str - expires_at: str - created_at: str - updated_at: str - - -class MagicAuth(MagicAuthCommon): - """Representation of a WorkOS MagicAuth object.""" - - code: str diff --git a/src/workos/types/user_management/oauth_tokens.py b/src/workos/types/user_management/oauth_tokens.py deleted file mode 100644 index 14b07a5c..00000000 --- a/src/workos/types/user_management/oauth_tokens.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Literal, Sequence - -from workos.types.workos_model import WorkOSModel - -OAuthTokensProvidersType = Literal[ - "AppleOauth", - "GitHubOauth", - "GoogleOauth", - "MicrosoftOauth", - "SalesforceOauth", -] - - -class OAuthTokens(WorkOSModel): - """Representation of a WorkOS Dashboard member impersonating a user""" - - provider: OAuthTokensProvidersType - access_token: str - refresh_token: str - expires_at: int - scopes: Sequence[str] diff --git a/src/workos/types/user_management/organization_membership.py b/src/workos/types/user_management/organization_membership.py deleted file mode 100644 index 9fa94321..00000000 --- a/src/workos/types/user_management/organization_membership.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import Any, Literal, Mapping, Optional, Sequence - -from typing_extensions import TypedDict - -from workos.types.workos_model import WorkOSModel -from workos.typing.literals import LiteralOrUntyped - -OrganizationMembershipStatus = Literal["active", "inactive", "pending"] - - -class BaseOrganizationMembership(WorkOSModel): - object: Literal["organization_membership"] - id: str - user_id: str - organization_id: str - organization_name: Optional[str] = None - status: LiteralOrUntyped[OrganizationMembershipStatus] - directory_managed: bool = False - created_at: str - updated_at: str - - -class OrganizationMembershipRole(TypedDict): - slug: str - - -class OrganizationMembership(BaseOrganizationMembership): - """Representation of an WorkOS Organization Membership.""" - - role: OrganizationMembershipRole - roles: Optional[Sequence[OrganizationMembershipRole]] = None - custom_attributes: Mapping[str, Any] = {} diff --git a/src/workos/types/user_management/password_hash_type.py b/src/workos/types/user_management/password_hash_type.py deleted file mode 100644 index ad97e510..00000000 --- a/src/workos/types/user_management/password_hash_type.py +++ /dev/null @@ -1,5 +0,0 @@ -from typing import Literal - -PasswordHashType = Literal[ - "bcrypt", "firebase-scrypt", "pbkdf2", "scrypt", "ssha", "argon2" -] diff --git a/src/workos/types/user_management/password_reset.py b/src/workos/types/user_management/password_reset.py deleted file mode 100644 index 916c140f..00000000 --- a/src/workos/types/user_management/password_reset.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Literal -from workos.types.workos_model import WorkOSModel - - -class PasswordResetCommon(WorkOSModel): - object: Literal["password_reset"] - id: str - user_id: str - email: str - expires_at: str - created_at: str - - -class PasswordReset(PasswordResetCommon): - """Representation of a WorkOS PasswordReset object.""" - - password_reset_token: str - password_reset_url: str diff --git a/src/workos/types/user_management/screen_hint.py b/src/workos/types/user_management/screen_hint.py deleted file mode 100644 index d7043a71..00000000 --- a/src/workos/types/user_management/screen_hint.py +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Literal - -ScreenHintType = Literal["sign-up", "sign-in"] diff --git a/src/workos/types/user_management/session.py b/src/workos/types/user_management/session.py deleted file mode 100644 index b8ba58d1..00000000 --- a/src/workos/types/user_management/session.py +++ /dev/null @@ -1,78 +0,0 @@ -from enum import Enum -from typing import Literal, Optional, Sequence, TypedDict, Union - -from workos.types.user_management.impersonator import Impersonator -from workos.types.user_management.user import User -from workos.types.workos_model import WorkOSModel - - -class AuthenticateWithSessionCookieFailureReason(Enum): - INVALID_JWT = "invalid_jwt" - INVALID_SESSION_COOKIE = "invalid_session_cookie" - NO_SESSION_COOKIE_PROVIDED = "no_session_cookie_provided" - - -class AuthenticateWithSessionCookieSuccessResponse(WorkOSModel): - authenticated: Literal[True] - session_id: str - organization_id: Optional[str] = None - role: Optional[str] = None - roles: Optional[Sequence[str]] = None - permissions: Optional[Sequence[str]] = None - user: User - impersonator: Optional[Impersonator] = None - entitlements: Optional[Sequence[str]] = None - feature_flags: Optional[Sequence[str]] = None - - -class AuthenticateWithSessionCookieErrorResponse(WorkOSModel): - authenticated: Literal[False] - reason: Union[AuthenticateWithSessionCookieFailureReason, str] - - -class RefreshWithSessionCookieSuccessResponse( - AuthenticateWithSessionCookieSuccessResponse -): - sealed_session: str - - -class RefreshWithSessionCookieErrorResponse(WorkOSModel): - authenticated: Literal[False] - reason: Union[AuthenticateWithSessionCookieFailureReason, str] - - -class SessionConfig(TypedDict, total=False): - seal_session: bool - cookie_password: str - - -AuthMethodType = Literal[ - "cross_app_auth", - "external_auth", - "impersonation", - "magic_code", - "migrated_session", - "oauth", - "passkey", - "password", - "sso", - "unknown", -] - - -class Session(WorkOSModel): - """Representation of a WorkOS User Management Session.""" - - object: Literal["session"] - id: str - user_id: str - organization_id: Optional[str] = None - status: str - auth_method: AuthMethodType - impersonator: Optional[Impersonator] = None - ip_address: Optional[str] = None - user_agent: Optional[str] = None - expires_at: str - ended_at: Optional[str] = None - created_at: str - updated_at: str diff --git a/src/workos/types/user_management/user.py b/src/workos/types/user_management/user.py deleted file mode 100644 index 1c8cf8a3..00000000 --- a/src/workos/types/user_management/user.py +++ /dev/null @@ -1,22 +0,0 @@ -from dataclasses import field -from typing import Literal, Optional -from workos.types.metadata import Metadata -from workos.types.workos_model import WorkOSModel - - -class User(WorkOSModel): - """Representation of a WorkOS User.""" - - object: Literal["user"] - id: str - email: str - first_name: Optional[str] = None - last_name: Optional[str] = None - email_verified: bool - profile_picture_url: Optional[str] = None - last_sign_in_at: Optional[str] = None - locale: Optional[str] = None - created_at: str - updated_at: str - external_id: Optional[str] = None - metadata: Metadata = field(default_factory=dict) diff --git a/src/workos/types/user_management/user_management_provider_type.py b/src/workos/types/user_management/user_management_provider_type.py deleted file mode 100644 index efb147dd..00000000 --- a/src/workos/types/user_management/user_management_provider_type.py +++ /dev/null @@ -1,11 +0,0 @@ -from typing import Literal - - -UserManagementProviderType = Literal[ - "authkit", - "AppleOAuth", - "GitHubOAuth", - "GoogleOAuth", - "MicrosoftOAuth", - "SalesforceOAuth", -] diff --git a/src/workos/types/vault/__init__.py b/src/workos/types/vault/__init__.py deleted file mode 100644 index 120f9f03..00000000 --- a/src/workos/types/vault/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .key import * -from .object import * diff --git a/src/workos/types/vault/key.py b/src/workos/types/vault/key.py deleted file mode 100644 index 3d164cd3..00000000 --- a/src/workos/types/vault/key.py +++ /dev/null @@ -1,25 +0,0 @@ -from typing import Dict -from pydantic import BaseModel, RootModel -from workos.types.workos_model import WorkOSModel - - -class KeyContext(RootModel[Dict[str, str]]): - pass - - -class DataKey(WorkOSModel): - id: str - key: str - - -class DataKeyPair(WorkOSModel): - context: KeyContext - data_key: DataKey - encrypted_keys: str - - -class DecodedKeys(BaseModel): - iv: bytes - tag: bytes - keys: str # Base64-encoded string - ciphertext: bytes diff --git a/src/workos/types/vault/object.py b/src/workos/types/vault/object.py deleted file mode 100644 index 403f1c1f..00000000 --- a/src/workos/types/vault/object.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import Optional - -from workos.types.workos_model import WorkOSModel -from workos.types.vault import KeyContext - - -class ObjectDigest(WorkOSModel): - id: str - name: str - updated_at: str - - -class ObjectUpdateBy(WorkOSModel): - id: str - name: str - - -class ObjectMetadata(WorkOSModel): - context: KeyContext - environment_id: str - id: str - key_id: str - updated_at: str - updated_by: ObjectUpdateBy - version_id: str - - -class VaultObject(WorkOSModel): - id: str - metadata: ObjectMetadata - name: str - value: Optional[str] = None - - -class ObjectVersion(WorkOSModel): - created_at: str - current_version: bool - id: str diff --git a/src/workos/types/webhooks/__init__.py b/src/workos/types/webhooks/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/workos/types/webhooks/webhook.py b/src/workos/types/webhooks/webhook.py deleted file mode 100644 index a7b42823..00000000 --- a/src/workos/types/webhooks/webhook.py +++ /dev/null @@ -1,456 +0,0 @@ -from typing import Literal, Union - -from pydantic import Field -from typing_extensions import Annotated - -from workos.types.directory_sync import DirectoryGroup -from workos.types.directory_sync.directory_user import DirectoryUser -from workos.types.api_keys import ApiKey -from workos.types.events.authentication_payload import ( - AuthenticationEmailVerificationFailedPayload, - AuthenticationEmailVerificationSucceededPayload, - AuthenticationMagicAuthFailedPayload, - AuthenticationMagicAuthSucceededPayload, - AuthenticationMfaFailedPayload, - AuthenticationMfaSucceededPayload, - AuthenticationOauthFailedPayload, - AuthenticationOauthSucceededPayload, - AuthenticationPasskeyFailedPayload, - AuthenticationPasskeySucceededPayload, - AuthenticationPasswordFailedPayload, - AuthenticationPasswordSucceededPayload, - AuthenticationRadarRiskDetectedPayload, - AuthenticationSsoFailedPayload, - AuthenticationSsoSucceededPayload, -) -from workos.types.events.connection_payload_with_legacy_fields import ( - ConnectionPayloadWithLegacyFields, -) -from workos.types.events.connection_saml_certificate_payload import ( - ConnectionSamlCertificateRenewedPayload, - ConnectionSamlCertificateRenewalRequiredPayload, -) -from workos.types.events.directory_group_membership_payload import ( - DirectoryGroupMembershipPayload, -) -from workos.types.events.directory_group_with_previous_attributes import ( - DirectoryGroupWithPreviousAttributes, -) -from workos.types.events.directory_payload import DirectoryPayload -from workos.types.events.directory_payload_with_legacy_fields import ( - DirectoryPayloadWithLegacyFields, -) -from workos.types.events.directory_user_with_previous_attributes import ( - DirectoryUserWithPreviousAttributes, -) -from workos.types.events.flag_payload import FlagPayload, FlagRuleUpdatedContext -from workos.types.events.organization_domain_verification_failed_payload import ( - OrganizationDomainVerificationFailedPayload, -) -from workos.types.events.session_payload import ( - SessionCreatedPayload, - SessionRevokedPayload, -) -from workos.types.authorization.organization_role import OrganizationRoleEvent -from workos.types.authorization.permission import Permission -from workos.types.organization_domains import OrganizationDomain -from workos.types.organizations.organization_common import OrganizationCommon -from workos.types.roles.role import EventRole -from workos.types.sso.connection import Connection -from workos.types.user_management import OrganizationMembership, User -from workos.types.user_management.email_verification import ( - EmailVerificationCommon, -) -from workos.types.user_management.invitation import InvitationCommon -from workos.types.user_management.magic_auth import MagicAuthCommon -from workos.types.user_management.password_reset import PasswordResetCommon -from workos.types.webhooks.webhook_model import WebhookModel - -# README -# When adding a new webhook event type, ensure the new webhook class is -# added to the Webhook union type at the bottom of this file. - - -class ApiKeyCreatedWebhook(WebhookModel[ApiKey]): - event: Literal["api_key.created"] - - -class ApiKeyRevokedWebhook(WebhookModel[ApiKey]): - event: Literal["api_key.revoked"] - - -class AuthenticationEmailVerificationFailedWebhook( - WebhookModel[AuthenticationEmailVerificationFailedPayload,] -): - event: Literal["authentication.email_verification_failed"] - - -class AuthenticationEmailVerificationSucceededWebhook( - WebhookModel[AuthenticationEmailVerificationSucceededPayload,] -): - event: Literal["authentication.email_verification_succeeded"] - - -class AuthenticationMagicAuthFailedWebhook( - WebhookModel[AuthenticationMagicAuthFailedPayload,] -): - event: Literal["authentication.magic_auth_failed"] - - -class AuthenticationMagicAuthSucceededWebhook( - WebhookModel[AuthenticationMagicAuthSucceededPayload,] -): - event: Literal["authentication.magic_auth_succeeded"] - - -class AuthenticationMfaFailedWebhook(WebhookModel[AuthenticationMfaFailedPayload]): - event: Literal["authentication.mfa_failed"] - - -class AuthenticationMfaSucceededWebhook( - WebhookModel[AuthenticationMfaSucceededPayload] -): - event: Literal["authentication.mfa_succeeded"] - - -class AuthenticationOauthFailedWebhook(WebhookModel[AuthenticationOauthFailedPayload]): - event: Literal["authentication.oauth_failed"] - - -class AuthenticationOauthSucceededWebhook( - WebhookModel[AuthenticationOauthSucceededPayload] -): - event: Literal["authentication.oauth_succeeded"] - - -class AuthenticationPasskeyFailedWebhook( - WebhookModel[AuthenticationPasskeyFailedPayload] -): - event: Literal["authentication.passkey_failed"] - - -class AuthenticationPasskeySucceededWebhook( - WebhookModel[AuthenticationPasskeySucceededPayload] -): - event: Literal["authentication.passkey_succeeded"] - - -class AuthenticationPasswordFailedWebhook( - WebhookModel[AuthenticationPasswordFailedPayload] -): - event: Literal["authentication.password_failed"] - - -class AuthenticationPasswordSucceededWebhook( - WebhookModel[AuthenticationPasswordSucceededPayload,] -): - event: Literal["authentication.password_succeeded"] - - -class AuthenticationRadarRiskDetectedWebhook( - WebhookModel[AuthenticationRadarRiskDetectedPayload] -): - event: Literal["authentication.radar_risk_detected"] - - -class AuthenticationSsoFailedWebhook(WebhookModel[AuthenticationSsoFailedPayload]): - event: Literal["authentication.sso_failed"] - - -class AuthenticationSsoSucceededWebhook( - WebhookModel[AuthenticationSsoSucceededPayload] -): - event: Literal["authentication.sso_succeeded"] - - -class ConnectionActivatedWebhook(WebhookModel[ConnectionPayloadWithLegacyFields]): - event: Literal["connection.activated"] - - -class ConnectionDeactivatedWebhook(WebhookModel[ConnectionPayloadWithLegacyFields]): - event: Literal["connection.deactivated"] - - -class ConnectionDeletedWebhook(WebhookModel[Connection]): - event: Literal["connection.deleted"] - - -class ConnectionSamlCertificateRenewedWebhook( - WebhookModel[ConnectionSamlCertificateRenewedPayload] -): - event: Literal["connection.saml_certificate_renewed"] - - -class ConnectionSamlCertificateRenewalRequiredWebhook( - WebhookModel[ConnectionSamlCertificateRenewalRequiredPayload] -): - event: Literal["connection.saml_certificate_renewal_required"] - - -class DirectoryActivatedWebhook(WebhookModel[DirectoryPayloadWithLegacyFields]): - event: Literal["dsync.activated"] - - -class DirectoryDeletedWebhook(WebhookModel[DirectoryPayload]): - event: Literal["dsync.deleted"] - - -class DirectoryGroupCreatedWebhook(WebhookModel[DirectoryGroup]): - event: Literal["dsync.group.created"] - - -class DirectoryGroupDeletedWebhook(WebhookModel[DirectoryGroup]): - event: Literal["dsync.group.deleted"] - - -class DirectoryGroupUpdatedWebhook(WebhookModel[DirectoryGroupWithPreviousAttributes]): - event: Literal["dsync.group.updated"] - - -class DirectoryUserCreatedWebhook(WebhookModel[DirectoryUser]): - event: Literal["dsync.user.created"] - - -class DirectoryUserDeletedWebhook(WebhookModel[DirectoryUser]): - event: Literal["dsync.user.deleted"] - - -class DirectoryUserUpdatedWebhook(WebhookModel[DirectoryUserWithPreviousAttributes]): - event: Literal["dsync.user.updated"] - - -class DirectoryUserAddedToGroupWebhook(WebhookModel[DirectoryGroupMembershipPayload]): - event: Literal["dsync.group.user_added"] - - -class DirectoryUserRemovedFromGroupWebhook( - WebhookModel[DirectoryGroupMembershipPayload] -): - event: Literal["dsync.group.user_removed"] - - -class EmailVerificationCreatedWebhook(WebhookModel[EmailVerificationCommon]): - event: Literal["email_verification.created"] - - -class FlagCreatedWebhook(WebhookModel[FlagPayload]): - event: Literal["flag.created"] - - -class FlagDeletedWebhook(WebhookModel[FlagPayload]): - event: Literal["flag.deleted"] - - -class FlagRuleUpdatedWebhook(WebhookModel[FlagPayload]): - event: Literal["flag.rule_updated"] - context: FlagRuleUpdatedContext - - -class FlagUpdatedWebhook(WebhookModel[FlagPayload]): - event: Literal["flag.updated"] - - -class InvitationAcceptedWebhook(WebhookModel[InvitationCommon]): - event: Literal["invitation.accepted"] - - -class InvitationCreatedWebhook(WebhookModel[InvitationCommon]): - event: Literal["invitation.created"] - - -class InvitationResentWebhook(WebhookModel[InvitationCommon]): - event: Literal["invitation.resent"] - - -class InvitationRevokedWebhook(WebhookModel[InvitationCommon]): - event: Literal["invitation.revoked"] - - -class MagicAuthCreatedWebhook(WebhookModel[MagicAuthCommon]): - event: Literal["magic_auth.created"] - - -class OrganizationCreatedWebhook(WebhookModel[OrganizationCommon]): - event: Literal["organization.created"] - - -class OrganizationDeletedWebhook(WebhookModel[OrganizationCommon]): - event: Literal["organization.deleted"] - - -class OrganizationUpdatedWebhook(WebhookModel[OrganizationCommon]): - event: Literal["organization.updated"] - - -class OrganizationDomainVerificationFailedWebhook( - WebhookModel[OrganizationDomainVerificationFailedPayload,] -): - event: Literal["organization_domain.verification_failed"] - - -class OrganizationDomainVerifiedWebhook(WebhookModel[OrganizationDomain]): - event: Literal["organization_domain.verified"] - - -class OrganizationDomainCreatedWebhook(WebhookModel[OrganizationDomain]): - event: Literal["organization_domain.created"] - - -class OrganizationDomainUpdatedWebhook(WebhookModel[OrganizationDomain]): - event: Literal["organization_domain.updated"] - - -class OrganizationDomainDeletedWebhook(WebhookModel[OrganizationDomain]): - event: Literal["organization_domain.deleted"] - - -class OrganizationMembershipCreatedWebhook(WebhookModel[OrganizationMembership]): - event: Literal["organization_membership.created"] - - -class OrganizationMembershipDeletedWebhook(WebhookModel[OrganizationMembership]): - event: Literal["organization_membership.deleted"] - - -class OrganizationMembershipUpdatedWebhook(WebhookModel[OrganizationMembership]): - event: Literal["organization_membership.updated"] - - -class OrganizationRoleCreatedWebhook(WebhookModel[OrganizationRoleEvent]): - event: Literal["organization_role.created"] - - -class OrganizationRoleUpdatedWebhook(WebhookModel[OrganizationRoleEvent]): - event: Literal["organization_role.updated"] - - -class OrganizationRoleDeletedWebhook(WebhookModel[OrganizationRoleEvent]): - event: Literal["organization_role.deleted"] - - -class PasswordResetCreatedWebhook(WebhookModel[PasswordResetCommon]): - event: Literal["password_reset.created"] - - -class PasswordResetSucceededWebhook(WebhookModel[PasswordResetCommon]): - event: Literal["password_reset.succeeded"] - - -class PermissionCreatedWebhook(WebhookModel[Permission]): - event: Literal["permission.created"] - - -class PermissionUpdatedWebhook(WebhookModel[Permission]): - event: Literal["permission.updated"] - - -class PermissionDeletedWebhook(WebhookModel[Permission]): - event: Literal["permission.deleted"] - - -class RoleCreatedWebhook(WebhookModel[EventRole]): - event: Literal["role.created"] - - -class RoleDeletedWebhook(WebhookModel[EventRole]): - event: Literal["role.deleted"] - - -class RoleUpdatedWebhook(WebhookModel[EventRole]): - event: Literal["role.updated"] - - -class SessionCreatedWebhook(WebhookModel[SessionCreatedPayload]): - event: Literal["session.created"] - - -class SessionRevokedWebhook(WebhookModel[SessionRevokedPayload]): - event: Literal["session.revoked"] - - -class UserCreatedWebhook(WebhookModel[User]): - event: Literal["user.created"] - - -class UserDeletedWebhook(WebhookModel[User]): - event: Literal["user.deleted"] - - -class UserUpdatedWebhook(WebhookModel[User]): - event: Literal["user.updated"] - - -Webhook = Annotated[ - Union[ - ApiKeyCreatedWebhook, - ApiKeyRevokedWebhook, - AuthenticationEmailVerificationFailedWebhook, - AuthenticationEmailVerificationSucceededWebhook, - AuthenticationMagicAuthFailedWebhook, - AuthenticationMagicAuthSucceededWebhook, - AuthenticationMfaFailedWebhook, - AuthenticationMfaSucceededWebhook, - AuthenticationOauthFailedWebhook, - AuthenticationOauthSucceededWebhook, - AuthenticationPasskeyFailedWebhook, - AuthenticationPasskeySucceededWebhook, - AuthenticationPasswordFailedWebhook, - AuthenticationPasswordSucceededWebhook, - AuthenticationRadarRiskDetectedWebhook, - AuthenticationSsoFailedWebhook, - AuthenticationSsoSucceededWebhook, - ConnectionActivatedWebhook, - ConnectionDeactivatedWebhook, - ConnectionDeletedWebhook, - ConnectionSamlCertificateRenewedWebhook, - ConnectionSamlCertificateRenewalRequiredWebhook, - DirectoryActivatedWebhook, - DirectoryDeletedWebhook, - DirectoryGroupCreatedWebhook, - DirectoryGroupDeletedWebhook, - DirectoryGroupUpdatedWebhook, - DirectoryUserCreatedWebhook, - DirectoryUserDeletedWebhook, - DirectoryUserUpdatedWebhook, - DirectoryUserAddedToGroupWebhook, - DirectoryUserRemovedFromGroupWebhook, - EmailVerificationCreatedWebhook, - FlagCreatedWebhook, - FlagDeletedWebhook, - FlagRuleUpdatedWebhook, - FlagUpdatedWebhook, - InvitationAcceptedWebhook, - InvitationCreatedWebhook, - InvitationResentWebhook, - InvitationRevokedWebhook, - MagicAuthCreatedWebhook, - OrganizationCreatedWebhook, - OrganizationDeletedWebhook, - OrganizationUpdatedWebhook, - OrganizationDomainCreatedWebhook, - OrganizationDomainDeletedWebhook, - OrganizationDomainUpdatedWebhook, - OrganizationDomainVerificationFailedWebhook, - OrganizationDomainVerifiedWebhook, - OrganizationMembershipCreatedWebhook, - OrganizationMembershipDeletedWebhook, - OrganizationMembershipUpdatedWebhook, - OrganizationRoleCreatedWebhook, - OrganizationRoleUpdatedWebhook, - OrganizationRoleDeletedWebhook, - PasswordResetCreatedWebhook, - PasswordResetSucceededWebhook, - PermissionCreatedWebhook, - PermissionUpdatedWebhook, - PermissionDeletedWebhook, - RoleCreatedWebhook, - RoleDeletedWebhook, - RoleUpdatedWebhook, - SessionCreatedWebhook, - SessionRevokedWebhook, - UserCreatedWebhook, - UserDeletedWebhook, - UserUpdatedWebhook, - ], - Field(..., discriminator="event"), -] diff --git a/src/workos/types/webhooks/webhook_model.py b/src/workos/types/webhooks/webhook_model.py deleted file mode 100644 index 74b1e367..00000000 --- a/src/workos/types/webhooks/webhook_model.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import Generic -from workos.types.events.event_model import EventPayload -from workos.types.workos_model import WorkOSModel - - -class WebhookModel(WorkOSModel, Generic[EventPayload]): - """Representation of an Webhook delivered via Webhook. - Attributes: - OBJECT_FIELDS (list): List of fields an Webhook is comprised of. - """ - - id: str - data: EventPayload - created_at: str diff --git a/src/workos/types/webhooks/webhook_payload.py b/src/workos/types/webhooks/webhook_payload.py deleted file mode 100644 index b6ce1153..00000000 --- a/src/workos/types/webhooks/webhook_payload.py +++ /dev/null @@ -1,4 +0,0 @@ -from typing import Union - - -WebhookPayload = Union[bytes, bytearray] diff --git a/src/workos/types/widgets/__init__.py b/src/workos/types/widgets/__init__.py deleted file mode 100644 index b0972fc1..00000000 --- a/src/workos/types/widgets/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .widget_scope import * -from .widget_token_response import * diff --git a/src/workos/types/widgets/widget_scope.py b/src/workos/types/widgets/widget_scope.py deleted file mode 100644 index 484f0977..00000000 --- a/src/workos/types/widgets/widget_scope.py +++ /dev/null @@ -1,4 +0,0 @@ -from typing import Literal - - -WidgetScope = Literal["widgets:users-table:manage"] diff --git a/src/workos/types/widgets/widget_token_response.py b/src/workos/types/widgets/widget_token_response.py deleted file mode 100644 index 3d3af0f0..00000000 --- a/src/workos/types/widgets/widget_token_response.py +++ /dev/null @@ -1,7 +0,0 @@ -from workos.types.workos_model import WorkOSModel - - -class WidgetTokenResponse(WorkOSModel): - """Representation of a WorkOS widget token response.""" - - token: str diff --git a/src/workos/types/workos_model.py b/src/workos/types/workos_model.py deleted file mode 100644 index 966cce9a..00000000 --- a/src/workos/types/workos_model.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Any, Dict, Optional -from typing_extensions import override -from pydantic import BaseModel -from pydantic.main import IncEx - - -class WorkOSModel(BaseModel): - @override - def dict( - self, - *, - include: Optional[IncEx] = None, - exclude: Optional[IncEx] = None, - by_alias: bool = False, - exclude_unset: bool = False, - exclude_defaults: bool = False, - exclude_none: bool = False, - ) -> Dict[str, Any]: - return self.model_dump( - include=include, - exclude=exclude, - by_alias=by_alias, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, - exclude_none=exclude_none, - ) diff --git a/src/workos/typing/__init__.py b/src/workos/typing/__init__.py deleted file mode 100644 index 6d76241d..00000000 --- a/src/workos/typing/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from workos.typing.untyped_literal import is_untyped_literal diff --git a/src/workos/typing/literals.py b/src/workos/typing/literals.py deleted file mode 100644 index b32dee28..00000000 --- a/src/workos/typing/literals.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import Any, TypeVar, Union -from typing_extensions import Annotated, LiteralString -from pydantic import ( - Field, - ValidationError, - ValidationInfo, - ValidatorFunctionWrapHandler, - WrapValidator, -) -from workos.typing.untyped_literal import UntypedLiteral - - -def convert_unknown_literal_to_untyped_literal( - value: Any, - handler: ValidatorFunctionWrapHandler, - info: ValidationInfo, - # TODO: Find a way to better type, give that the last case in the except block truly can return Any -) -> Union[LiteralString, UntypedLiteral, Any]: - try: - return handler(value) - except ValidationError as validation_error: - if validation_error.errors()[0]["type"] == "literal_error": - return handler(UntypedLiteral(value)) - else: - return handler(value) - - -LiteralType = TypeVar("LiteralType", bound=LiteralString) -LiteralOrUntyped = Annotated[ - Annotated[Union[LiteralType, UntypedLiteral], Field(union_mode="left_to_right")], - WrapValidator(convert_unknown_literal_to_untyped_literal), -] diff --git a/src/workos/typing/sync_or_async.py b/src/workos/typing/sync_or_async.py deleted file mode 100644 index d336c76e..00000000 --- a/src/workos/typing/sync_or_async.py +++ /dev/null @@ -1,5 +0,0 @@ -from typing import Awaitable, TypeVar, Union - - -T = TypeVar("T") -SyncOrAsync = Union[T, Awaitable[T]] diff --git a/src/workos/typing/untyped_literal.py b/src/workos/typing/untyped_literal.py deleted file mode 100644 index 3ee424dc..00000000 --- a/src/workos/typing/untyped_literal.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import Any -from pydantic_core import CoreSchema, core_schema -from pydantic import GetCoreSchemaHandler - - -class UntypedLiteral(str): - def __new__(cls, value: str) -> "UntypedLiteral": - return super().__new__(cls, f"Untyped[{value}]") - - @classmethod - def validate_untyped_literal(cls, value: Any) -> Any: - if isinstance(value, UntypedLiteral): - return value - else: - # TODO: Should this raise an error that translates to pydantic's is_instance_of error? - raise ValueError("Value is not an instance of UntypedLiteral") - - @classmethod - def __get_pydantic_core_schema__( - cls, source_type: Any, handler: GetCoreSchemaHandler - ) -> CoreSchema: - return core_schema.no_info_plain_validator_function( - function=cls.validate_untyped_literal, - ) - - -# TypeGuard doesn't actually work for exhaustiveness checking, but we can return a boolean expression instead -# https://github.com/python/mypy/issues/15305 -# TODO: see if there is a way to define this as TypeGuard, TypeIs, or bool depending on python version -# def is_untyped_literal(value: Union[str, UntypedLiteral]) -> TypeGuard[UntypedLiteral]: -# return isinstance(value, UntypedLiteral) - - -def is_untyped_literal(value: Any) -> bool: - # A helper to detect untyped values from the API (more explainer here) - # Does not help with exhaustiveness checking - return isinstance(value, UntypedLiteral) diff --git a/src/workos/typing/webhooks.py b/src/workos/typing/webhooks.py deleted file mode 100644 index 681ea568..00000000 --- a/src/workos/typing/webhooks.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Any, Dict, Union -from typing_extensions import Annotated -from pydantic import Field, TypeAdapter -from workos.types.webhooks.webhook import Webhook -from workos.types.workos_model import WorkOSModel - - -# Fall back to untyped Webhook if the event type is not recognized -class UntypedWebhook(WorkOSModel): - id: str - event: str - data: Dict[str, Any] - created_at: str - - -WebhookTypeAdapter: TypeAdapter[Webhook] = TypeAdapter( - Annotated[Union[Webhook, UntypedWebhook], Field(union_mode="left_to_right")], -) diff --git a/src/workos/user_management.py b/src/workos/user_management.py deleted file mode 100644 index b8a72d62..00000000 --- a/src/workos/user_management.py +++ /dev/null @@ -1,2428 +0,0 @@ -from typing import Awaitable, Optional, Protocol, Sequence, Type, Union, cast -from urllib.parse import urlencode - -from workos._client_configuration import ClientConfiguration -from workos.session import AsyncSession, Session -from workos.types.feature_flags import FeatureFlag -from workos.types.feature_flags.list_filters import FeatureFlagListFilters -from workos.types.list_resource import ( - ListArgs, - ListMetadata, - ListPage, - WorkOSListResource, -) -from workos.types.metadata import Metadata -from workos.types.mfa import ( - AuthenticationFactor, - AuthenticationFactorTotpAndChallengeResponse, - AuthenticationFactorType, -) -from workos.types.user_management import ( - AuthenticationResponse, - EmailVerification, - Invitation, - MagicAuth, - OrganizationMembership, - OrganizationMembershipStatus, - PasswordReset, - RefreshTokenAuthenticationResponse, - User, -) -from workos.types.user_management.authenticate_with_common import ( - AuthenticateWithCodeParameters, - AuthenticateWithEmailVerificationParameters, - AuthenticateWithMagicAuthParameters, - AuthenticateWithOrganizationSelectionParameters, - AuthenticateWithParameters, - AuthenticateWithPasswordParameters, - AuthenticateWithRefreshTokenParameters, - AuthenticateWithTotpParameters, -) -from workos.types.user_management.authentication_response import ( - AuthenticationResponseType, - AuthKitAuthenticationResponse, -) -from workos.types.user_management.list_filters import ( - AuthenticationFactorsListFilters, - InvitationsListFilters, - OrganizationMembershipsListFilters, - SessionsListFilters, - UsersListFilters, -) -from workos.types.user_management.password_hash_type import PasswordHashType -from workos.types.user_management.screen_hint import ScreenHintType -from workos.types.user_management.session import Session as UserManagementSession -from workos.types.user_management.session import SessionConfig -from workos.types.user_management.user_management_provider_type import ( - UserManagementProviderType, -) -from workos.typing.sync_or_async import SyncOrAsync -from workos.utils.http_client import AsyncHTTPClient, SyncHTTPClient -from workos.utils.pagination_order import PaginationOrder -from workos.utils.request_helper import ( - DEFAULT_LIST_RESPONSE_LIMIT, - REQUEST_METHOD_DELETE, - REQUEST_METHOD_GET, - REQUEST_METHOD_POST, - REQUEST_METHOD_PUT, - RESPONSE_TYPE_CODE, - QueryParameters, - RequestHelper, -) - -USER_PATH = "user_management/users" -USER_DETAIL_PATH = "user_management/users/{0}" -USER_DETAIL_BY_EXTERNAL_ID_PATH = "user_management/users/external_id/{0}" -ORGANIZATION_MEMBERSHIP_PATH = "user_management/organization_memberships" -ORGANIZATION_MEMBERSHIP_DETAIL_PATH = "user_management/organization_memberships/{0}" -ORGANIZATION_MEMBERSHIP_DEACTIVATE_PATH = ( - ORGANIZATION_MEMBERSHIP_DETAIL_PATH + "/deactivate" -) -ORGANIZATION_MEMBERSHIP_REACTIVATE_PATH = ( - ORGANIZATION_MEMBERSHIP_DETAIL_PATH + "/reactivate" -) -USER_AUTHORIZATION_PATH = "user_management/authorize" -USER_AUTHENTICATE_PATH = "user_management/authenticate" -USER_SEND_PASSWORD_RESET_PATH = "user_management/password_reset/send" -USER_RESET_PASSWORD_PATH = "user_management/password_reset/confirm" -USER_SEND_VERIFICATION_EMAIL_PATH = "user_management/users/{0}/email_verification/send" -USER_VERIFY_EMAIL_CODE_PATH = "user_management/users/{0}/email_verification/confirm" -MAGIC_AUTH_DETAIL_PATH = "user_management/magic_auth/{0}" -MAGIC_AUTH_PATH = "user_management/magic_auth" -USER_SEND_MAGIC_AUTH_PATH = "user_management/magic_auth/send" -USER_AUTH_FACTORS_PATH = "user_management/users/{0}/auth_factors" -USER_SESSIONS_PATH = "user_management/users/{0}/sessions" -SESSIONS_REVOKE_PATH = "user_management/sessions/revoke" -EMAIL_VERIFICATION_DETAIL_PATH = "user_management/email_verification/{0}" -INVITATION_PATH = "user_management/invitations" -INVITATION_DETAIL_PATH = "user_management/invitations/{0}" -INVITATION_DETAIL_BY_TOKEN_PATH = "user_management/invitations/by_token/{0}" -INVITATION_REVOKE_PATH = "user_management/invitations/{0}/revoke" -INVITATION_RESEND_PATH = "user_management/invitations/{0}/resend" -INVITATION_ACCEPT_PATH = "user_management/invitations/{0}/accept" -PASSWORD_RESET_PATH = "user_management/password_reset" -PASSWORD_RESET_DETAIL_PATH = "user_management/password_reset/{0}" -USER_FEATURE_FLAGS_PATH = "user_management/users/{0}/feature-flags" - - -UsersListResource = WorkOSListResource[User, UsersListFilters, ListMetadata] - -OrganizationMembershipsListResource = WorkOSListResource[ - OrganizationMembership, OrganizationMembershipsListFilters, ListMetadata -] - -AuthenticationFactorsListResource = WorkOSListResource[ - AuthenticationFactor, AuthenticationFactorsListFilters, ListMetadata -] - -InvitationsListResource = WorkOSListResource[ - Invitation, InvitationsListFilters, ListMetadata -] - -FeatureFlagsListResource = WorkOSListResource[ - FeatureFlag, FeatureFlagListFilters, ListMetadata -] - -SessionsListResource = WorkOSListResource[ - UserManagementSession, SessionsListFilters, ListMetadata -] - - -class UserManagementModule(Protocol): - """Offers methods for using the WorkOS User Management API.""" - - _client_configuration: ClientConfiguration - - def load_sealed_session( - self, *, sealed_session: str, cookie_password: str - ) -> Union[Session, Awaitable[AsyncSession]]: - """Load a sealed session and return the session data. - - Args: - sealed_session (str): The sealed session data to load. - cookie_password (str): The cookie password to use to decrypt the session data. - - Returns: - Session: The session module. - """ - ... - - def get_user(self, user_id: str) -> SyncOrAsync[User]: - """Get the details of an existing user. - - Args: - user_id (str): User unique identifier - Returns: - User: User response from WorkOS. - """ - ... - - def get_user_by_external_id(self, external_id: str) -> SyncOrAsync[User]: - """Get the details of an existing user by external id. - - Args: - external_id (str): User's external id - Returns: - User: User response from WorkOS. - """ - ... - - def list_users( - self, - *, - email: Optional[str] = None, - organization_id: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[UsersListResource]: - """Get a list of all of your existing users matching the criteria specified. - - Kwargs: - email (str): Filter Users by their email. (Optional) - organization_id (str): Filter Users by the organization they are members of. (Optional) - limit (int): Maximum number of records to return. (Optional) - before (str): Pagination cursor to receive records before a provided User ID. (Optional) - after (str): Pagination cursor to receive records after a provided User ID. (Optional) - order (Literal["asc","desc"]): Sort records in either ascending or descending (default) order by created_at timestamp. (Optional) - - Returns: - UsersListResource: Users response from WorkOS. - """ - ... - - def create_user( - self, - *, - email: str, - password: Optional[str] = None, - password_hash: Optional[str] = None, - password_hash_type: Optional[PasswordHashType] = None, - first_name: Optional[str] = None, - last_name: Optional[str] = None, - email_verified: Optional[bool] = None, - external_id: Optional[str] = None, - metadata: Optional[Metadata] = None, - ) -> SyncOrAsync[User]: - """Create a new user. - - Kwargs: - email (str): The email address of the user. - password (str): The password to set for the user. (Optional) - password_hash (str): The hashed password to set for the user. Mutually exclusive with password. (Optional) - password_hash_type (str): The algorithm originally used to hash the password, used when providing a password_hash. Valid values are 'bcrypt', `firebase-scrypt`, and `ssha`. (Optional) - first_name (str): The user's first name. (Optional) - last_name (str): The user's last name. (Optional) - email_verified (bool): Whether the user's email address was previously verified. (Optional) - - Returns: - User: Created User response from WorkOS. - """ - ... - - def update_user( - self, - *, - user_id: str, - email: Optional[str] = None, - first_name: Optional[str] = None, - last_name: Optional[str] = None, - email_verified: Optional[bool] = None, - password: Optional[str] = None, - password_hash: Optional[str] = None, - password_hash_type: Optional[PasswordHashType] = None, - external_id: Optional[str] = None, - metadata: Optional[Metadata] = None, - locale: Optional[str] = None, - ) -> SyncOrAsync[User]: - """Update user attributes. - - Kwargs: - user_id (str): The User unique identifier - first_name (str): The user's first name. (Optional) - last_name (str): The user's last name. (Optional) - email (str): The user's email. (Optional) - email_verified (bool): Whether the user's email address was previously verified. (Optional) - password (str): The password to set for the user. (Optional) - password_hash (str): The hashed password to set for the user, used when migrating from another user store. Mutually exclusive with password. (Optional) - password_hash_type (str): The algorithm originally used to hash the password, used when providing a password_hash. Valid values are 'bcrypt', `firebase-scrypt`, and `ssha`. (Optional) - locale (str): The user's locale. (Optional) - - Returns: - User: Updated User response from WorkOS. - """ - ... - - def delete_user(self, user_id: str) -> SyncOrAsync[None]: - """Delete an existing user. - - Args: - user_id (str): User unique identifier - Returns: - None - """ - ... - - def create_organization_membership( - self, - *, - user_id: str, - organization_id: str, - role_slug: Optional[str] = None, - role_slugs: Optional[Sequence[str]] = None, - ) -> SyncOrAsync[OrganizationMembership]: - """Create a new OrganizationMembership for the given Organization and User. - - Kwargs: - user_id: The unique ID of the User. - organization_id: The unique ID of the Organization to which the user belongs to. - role_slug: The unique slug of the role to grant to this membership.(Optional) - role_slugs: The unique slugs of the roles to grant to this membership.(Optional) - - Note: - role_slug and role_slugs are mutually exclusive. If neither is provided, - the user will be assigned the organization's default role. - - Returns: - OrganizationMembership: Created OrganizationMembership response from WorkOS. - """ - ... - - def update_organization_membership( - self, - *, - organization_membership_id: str, - role_slug: Optional[str] = None, - role_slugs: Optional[Sequence[str]] = None, - ) -> SyncOrAsync[OrganizationMembership]: - """Updates an OrganizationMembership for the given id. - - Args: - organization_membership_id (str): The unique ID of the Organization Membership. - role_slug: The unique slug of the role to grant to this membership.(Optional) - role_slugs: The unique slugs of the roles to grant to this membership.(Optional) - - Note: - role_slug and role_slugs are mutually exclusive. If neither is provided, - the role(s) of the membership will remain unchanged. - - Returns: - OrganizationMembership: Updated OrganizationMembership response from WorkOS. - """ - ... - - def get_organization_membership( - self, organization_membership_id: str - ) -> SyncOrAsync[OrganizationMembership]: - """Get the details of an organization membership. - - Args: - organization_membership_id (str): The unique ID of the Organization Membership. - Returns: - OrganizationMembership: OrganizationMembership response from WorkOS. - """ - ... - - def list_organization_memberships( - self, - *, - user_id: Optional[str] = None, - organization_id: Optional[str] = None, - statuses: Optional[Sequence[OrganizationMembershipStatus]] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[OrganizationMembershipsListResource]: - """Get a list of all of your existing organization memberships matching the criteria specified. - - Kwargs: - user_id (str): Filter Organization Memberships by user. (Optional) - organization_id (str): Filter Organization Memberships by organization. (Optional) - statuses (Sequence[OrganizationMembershipStatus]): Filter Organization Memberships by status. (Optional) - limit (int): Maximum number of records to return. (Optional) - before (str): Pagination cursor to receive records before a provided Organization Membership ID. (Optional) - after (str): Pagination cursor to receive records after a provided Organization Membership ID. (Optional) - order (Literal["asc","desc"]): Sort records in either ascending or descending (default) order by created_at timestamp. (Optional) - - Returns: - OrganizationMembershipsListResource: Organization Memberships response from WorkOS. - """ - ... - - def delete_organization_membership( - self, organization_membership_id: str - ) -> SyncOrAsync[None]: - """Delete an existing organization membership. - - Args: - organization_membership_id (str): The unique ID of the Organization Membership. - Returns: - None - """ - ... - - def deactivate_organization_membership( - self, organization_membership_id: str - ) -> SyncOrAsync[OrganizationMembership]: - """Deactivate an organization membership. - - Args: - organization_membership_id (str): The unique ID of the Organization Membership. - Returns: - OrganizationMembership: OrganizationMembership response from WorkOS. - """ - ... - - def reactivate_organization_membership( - self, organization_membership_id: str - ) -> SyncOrAsync[OrganizationMembership]: - """Reactivates an organization membership. - - Args: - organization_membership_id (str): The unique ID of the Organization Membership. - Returns: - OrganizationMembership: OrganizationMembership response from WorkOS. - """ - ... - - def get_authorization_url( - self, - *, - redirect_uri: str, - domain_hint: Optional[str] = None, - login_hint: Optional[str] = None, - state: Optional[str] = None, - provider: Optional[UserManagementProviderType] = None, - provider_scopes: Optional[Sequence[str]] = None, - connection_id: Optional[str] = None, - organization_id: Optional[str] = None, - code_challenge: Optional[str] = None, - prompt: Optional[str] = None, - screen_hint: Optional[ScreenHintType] = None, - ) -> str: - """Generate an OAuth 2.0 authorization URL. - - The URL generated will redirect a User to the Identity Provider configured through - WorkOS. - - This method is purposefully designed as synchronous as it does not make any HTTP requests. - - Kwargs: - redirect_uri (str): A Redirect URI to return an authorized user to. - connection_id (str): The connection_id connection selector is used to initiate SSO for a Connection. - The value of this parameter should be a WorkOS Connection ID. (Optional) - organization_id (str): The organization_id connection selector is used to initiate SSO for an Organization. - The value of this parameter should be a WorkOS Organization ID. (Optional) - provider (UserManagementProviderType): The provider connection selector is used to initiate SSO using an OAuth-compatible provider. - Currently, the supported values for provider are 'authkit', 'AppleOAuth', 'GitHubOAuth, 'GoogleOAuth', 'MicrosoftOAuth', and 'SalesforceOAuth'. (Optional) - provider_scopes (Sequence[str]): Can be used to specify additional scopes that will be requested when initiating SSO using an OAuth provider. (Optional) - domain_hint (str): Can be used to pre-fill the domain field when initiating authentication with Microsoft OAuth, - or with a GoogleSAML connection type. (Optional) - login_hint (str): Can be used to pre-fill the username/email address field of the IdP sign-in page for the user, - if you know their username ahead of time. Currently, this parameter is supported for OAuth, OpenID Connect, - OktaSAML, and AzureSAML connection types. (Optional) - state (str): An encoded string passed to WorkOS that'd be preserved through the authentication workflow, passed - back as a query parameter. (Optional) - code_challenge (str): Code challenge is derived from the code verifier used for the PKCE flow. (Optional) - prompt (str): Used to specify whether the upstream provider should prompt the user for credentials or other - consent. Valid values depend on the provider. Currently only applies to provider values of 'GoogleOAuth', - 'MicrosoftOAuth', or 'GitHubOAuth'. (Optional) - screen_hint (ScreenHintType): Specify which AuthKit screen users should land on upon redirection (Only applicable when provider is 'authkit'). - - Returns: - str: URL to redirect a User to to begin the OAuth workflow with WorkOS - """ - params: QueryParameters = { - "client_id": self._client_configuration.client_id, - "redirect_uri": redirect_uri, - "response_type": RESPONSE_TYPE_CODE, - } - - if connection_id is None and organization_id is None and provider is None: - raise ValueError( - "Incomplete arguments. Need to specify either a 'connection_id', 'organization_id', or 'provider_id'" - ) - - if connection_id is not None: - params["connection_id"] = connection_id - if organization_id is not None: - params["organization_id"] = organization_id - if provider is not None: - params["provider"] = provider - if provider_scopes is not None: - params["provider_scopes"] = ",".join(provider_scopes) - if domain_hint is not None: - params["domain_hint"] = domain_hint - if login_hint is not None: - params["login_hint"] = login_hint - if state is not None: - params["state"] = state - if code_challenge: - params["code_challenge"] = code_challenge - params["code_challenge_method"] = "S256" - if prompt is not None: - params["prompt"] = prompt - if screen_hint is not None: - params["screen_hint"] = screen_hint - - return RequestHelper.build_url_with_query_params( - base_url=self._client_configuration.base_url, - path=USER_AUTHORIZATION_PATH, - **params, - ) - - def _authenticate_with( - self, - payload: AuthenticateWithParameters, - response_model: Type[AuthenticationResponseType], - ) -> SyncOrAsync[AuthenticationResponseType]: ... - - def authenticate_with_password( - self, - *, - email: str, - password: str, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - ) -> SyncOrAsync[AuthenticationResponse]: - """Authenticates a user with email and password. - - Kwargs: - email (str): The email address of the user. - password (str): The password of the user. - ip_address (str): The IP address of the request from the user who is attempting to authenticate. (Optional) - user_agent (str): The user agent of the request from the user who is attempting to authenticate. (Optional) - - Returns: - AuthenticationResponse: Authentication response from WorkOS. - """ - ... - - def authenticate_with_code( - self, - *, - code: str, - session: Optional[SessionConfig] = None, - code_verifier: Optional[str] = None, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - invitation_token: Optional[str] = None, - ) -> SyncOrAsync[AuthenticationResponse]: - """Authenticates an OAuth user or a user that is logging in through SSO. - - Kwargs: - code (str): The authorization value which was passed back as a query parameter in the callback to the Redirect URI. - session (SessionConfig): Configuration for the session. (Optional) - code_verifier (str): The randomly generated string used to derive the code challenge that was passed to the authorization - url as part of the PKCE flow. This parameter is required when the client secret is not present. (Optional) - ip_address (str): The IP address of the request from the user who is attempting to authenticate. (Optional) - user_agent (str): The user agent of the request from the user who is attempting to authenticate. (Optional) - invitation_token (str): The token of an Invitation, if required. (Optional) - - Returns: - AuthenticationResponse: Authentication response from WorkOS. - """ - - ... - - def authenticate_with_magic_auth( - self, - *, - code: str, - email: str, - link_authorization_code: Optional[str] = None, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - ) -> SyncOrAsync[AuthenticationResponse]: - """Authenticates a user by verifying a one-time code sent to the user's email address by the Magic Auth Send Code endpoint. - - Kwargs: - code (str): The one-time code that was emailed to the user. - email (str): The email of the User who will be authenticated. - link_authorization_code (str): An authorization code used in a previous authenticate request that resulted in an existing user error response. (Optional) - ip_address (str): The IP address of the request from the user who is attempting to authenticate. (Optional) - user_agent (str): The user agent of the request from the user who is attempting to authenticate. (Optional) - - Returns: - AuthenticationResponse: Authentication response from WorkOS. - """ - ... - - def authenticate_with_email_verification( - self, - *, - code: str, - pending_authentication_token: str, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - ) -> SyncOrAsync[AuthenticationResponse]: - """Authenticates a user that requires email verification by verifying a one-time code sent to the user's email address and the pending authentication token. - - Kwargs: - code (str): The one-time code that was emailed to the user. - pending_authentication_token (str): The token returned from an authentication attempt due to an unverified email address. - ip_address (str): The IP address of the request from the user who is attempting to authenticate. (Optional) - user_agent (str): The user agent of the request from the user who is attempting to authenticate. (Optional) - - Returns: - AuthenticationResponse: Authentication response from WorkOS. - """ - ... - - def authenticate_with_totp( - self, - *, - code: str, - authentication_challenge_id: str, - pending_authentication_token: str, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - ) -> SyncOrAsync[AuthenticationResponse]: - """Authenticates a user that has MFA enrolled by verifying the TOTP code, the Challenge from the Factor, and the pending authentication token. - - Kwargs: - code (str): The time-based-one-time-password generated by the Factor that was challenged. - authentication_challenge_id (str): The unique ID of the authentication Challenge created for the TOTP Factor for which the user is enrolled. - pending_authentication_token (str): The token returned from a failed authentication attempt due to MFA challenge. - ip_address (str): The IP address of the request from the user who is attempting to authenticate. (Optional) - user_agent (str): The user agent of the request from the user who is attempting to authenticate. (Optional) - - Returns: - AuthenticationResponse: Authentication response from WorkOS. - """ - ... - - def authenticate_with_organization_selection( - self, - *, - organization_id: str, - pending_authentication_token: str, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - ) -> SyncOrAsync[AuthenticationResponse]: - """Authenticates a user that is a member of multiple organizations by verifying the organization ID and the pending authentication token. - - Kwargs: - organization_id (str): The organization to authenticate for. - pending_authentication_token (str): The token returned from a failed authentication attempt due to organization selection being required. - ip_address (str): The IP address of the request from the user who is attempting to authenticate. (Optional) - user_agent (str): The user agent of the request from the user who is attempting to authenticate. (Optional) - - Returns: - AuthenticationResponse: Authentication response from WorkOS. - """ - ... - - def authenticate_with_refresh_token( - self, - *, - refresh_token: str, - session: Optional[SessionConfig] = None, - organization_id: Optional[str] = None, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - ) -> SyncOrAsync[RefreshTokenAuthenticationResponse]: - """Authenticates a user with a refresh token. - - Kwargs: - refresh_token (str): The token associated to the user. - session (SessionConfig): Configuration for the session. (Optional) - organization_id (str): The organization to issue the new access token for. (Optional) - ip_address (str): The IP address of the request from the user who is attempting to authenticate. (Optional) - user_agent (str): The user agent of the request from the user who is attempting to authenticate. (Optional) - - Returns: - RefreshTokenAuthenticationResponse: Refresh Token Authentication response from WorkOS. - """ - ... - - def get_jwks_url(self) -> str: - """Get the public key that is used for verifying access tokens. - - This method is purposefully designed as synchronous as it does not make any HTTP requests. - - Returns: - (str): The public JWKS URL. - """ - - return f"{self._client_configuration.base_url}sso/jwks/{self._client_configuration.client_id}" - - def get_logout_url(self, session_id: str, return_to: Optional[str] = None) -> str: - """Get the URL for ending the session and redirecting the user - - This method is purposefully designed as synchronous as it does not make any HTTP requests. - - Args: - session_id (str): The ID of the user's session - return_to (str): The URL to redirect the user to after the session is ended. (Optional) - - Returns: - (str): URL to redirect the user to to end the session. - """ - - params = {"session_id": session_id} - - if return_to: - params["return_to"] = return_to - - return f"{self._client_configuration.base_url}user_management/sessions/logout?{urlencode(params)}" - - def get_password_reset(self, password_reset_id: str) -> SyncOrAsync[PasswordReset]: - """Get the details of a password reset object. - - Args: - password_reset_id (str): The unique ID of the password reset object. - - Returns: - PasswordReset: PasswordReset response from WorkOS. - """ - - ... - - def create_password_reset(self, email: str) -> SyncOrAsync[PasswordReset]: - """Creates a password reset token that can be sent to a user's email to reset the password. - - Args: - email: The email address of the user. - - Returns: - PasswordReset: PasswordReset response from WorkOS. - """ - ... - - def reset_password(self, *, token: str, new_password: str) -> SyncOrAsync[User]: - """Resets user password using token that was sent to the user. - - Kwargs: - token (str): The reset token emailed to the user. - new_password (str): The new password to be set for the user. - - Returns: - User: User response from WorkOS. - """ - ... - - def get_email_verification( - self, email_verification_id: str - ) -> SyncOrAsync[EmailVerification]: - """Get the details of an email verification object. - - Args: - email_verification_id (str): The unique ID of the email verification object. - - Returns: - EmailVerification: EmailVerification response from WorkOS. - """ - ... - - def send_verification_email(self, user_id: str) -> SyncOrAsync[User]: - """Sends a verification email to the provided user. - - Args: - user_id (str): The unique ID of the User whose email address will be verified. - - Returns: - User: User response from WorkOS. - """ - ... - - def verify_email(self, *, user_id: str, code: str) -> SyncOrAsync[User]: - """Verifies user email using one-time code that was sent to the user. - - Kwargs: - user_id (str): The unique ID of the User whose email address will be verified. - code (str): The one-time code emailed to the user. - - Returns: - User: User response from WorkOS. - """ - ... - - def list_sessions( - self, - *, - user_id: str, - limit: Optional[int] = None, - before: Optional[str] = None, - after: Optional[str] = None, - order: Optional[PaginationOrder] = "desc", - ) -> SyncOrAsync["SessionsListResource"]: ... - - def revoke_session(self, *, session_id: str) -> SyncOrAsync[None]: ... - - def get_magic_auth(self, magic_auth_id: str) -> SyncOrAsync[MagicAuth]: - """Get the details of a Magic Auth object. - - Args: - magic_auth_id (str): The unique ID of the Magic Auth object. - - Returns: - MagicAuth: MagicAuth response from WorkOS. - """ - ... - - def create_magic_auth( - self, *, email: str, invitation_token: Optional[str] = None - ) -> SyncOrAsync[MagicAuth]: - """Creates a Magic Auth code challenge that can be sent to a user's email for authentication. - - Kwargs: - email: The email address of the user. - invitation_token: The token of an Invitation, if required. (Optional) - - Returns: - MagicAuth: MagicAuth response from WorkOS. - """ - ... - - def enroll_auth_factor( - self, - *, - user_id: str, - type: AuthenticationFactorType, - totp_issuer: Optional[str] = None, - totp_user: Optional[str] = None, - totp_secret: Optional[str] = None, - ) -> SyncOrAsync[AuthenticationFactorTotpAndChallengeResponse]: - """Enrolls a user in a new auth factor. - - Kwargs: - user_id (str): The unique ID of the User to be enrolled in the auth factor. - type (str): The type of factor to enroll (Only option available is 'totp'). - totp_issuer (str): Name of the Organization (Optional) - totp_user (str): Email of user (Optional) - totp_secret (str): The secret key for the TOTP factor. Generated if not provided. (Optional) - - Returns: - AuthenticationFactorTotpAndChallengeResponse - """ - ... - - def list_auth_factors( - self, - *, - user_id: str, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[AuthenticationFactorsListResource]: - """Lists the Auth Factors for a user. - - Kwargs: - user_id (str): The unique ID of the User to list the auth factors for. - limit (int): Maximum number of records to return. (Optional) - before (str): Pagination cursor to receive records before a provided AuthenticationFactor ID. (Optional) - after (str): Pagination cursor to receive records after a provided AuthenticationFactor ID. (Optional) - order (Literal["asc","desc"]): Sort records in either ascending or descending order by created_at timestamp.(Optional) - Returns: - AuthenticationFactorsListResource: List of Authentication Factors for a User from WorkOS. - """ - ... - - def get_invitation(self, invitation_id: str) -> SyncOrAsync[Invitation]: - """Get the details of an invitation. - - Args: - invitation_id (str): The unique ID of the Invitation. - - Returns: - Invitation: Invitation response from WorkOS. - """ - ... - - def find_invitation_by_token( - self, invitation_token: str - ) -> SyncOrAsync[Invitation]: - """Get the details of an invitation. - - Args: - invitation_token (str): The token of the Invitation. - - Returns: - Invitation: Invitation response from WorkOS. - """ - ... - - def list_invitations( - self, - *, - email: Optional[str] = None, - organization_id: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[InvitationsListResource]: - """Get a list of all of your existing invitations matching the criteria specified. - - Kwargs: - email (str): Filter Invitations by email. (Optional) - organization_id (str): Filter Invitations by organization. (Optional) - limit (int): Maximum number of records to return. (Optional) - before (str): Pagination cursor to receive records before a provided Invitation ID. (Optional) - after (str): Pagination cursor to receive records after a provided Invitation ID. (Optional) - order (Literal["asc","desc"]): Sort records in either ascending or descending order by created_at timestamp. (Optional) - - Returns: - InvitationsListResource: Invitations list response from WorkOS. - """ - ... - - def send_invitation( - self, - *, - email: str, - organization_id: Optional[str] = None, - expires_in_days: Optional[int] = None, - inviter_user_id: Optional[str] = None, - role_slug: Optional[str] = None, - ) -> SyncOrAsync[Invitation]: - """Sends an Invitation to a recipient. - - Kwargs: - email: The email address of the recipient. - organization_id: The ID of the Organization to which the recipient is being invited. (Optional) - expires_in_days: The number of days the invitations will be valid for. Must be between 1 and 30, defaults to 7 if not specified. (Optional) - inviter_user_id: The ID of the User sending the invitation. (Optional) - role_slug: The unique slug of the Role to give the Membership once the invite is accepted (Optional) - - Returns: - Invitation: Sent Invitation response from WorkOS. - """ - ... - - def revoke_invitation(self, invitation_id: str) -> SyncOrAsync[Invitation]: - """Revokes an existing Invitation. - - Args: - invitation_id (str): The unique ID of the Invitation. - - Returns: - Invitation: Invitation response from WorkOS. - """ - ... - - def resend_invitation(self, invitation_id: str) -> SyncOrAsync[Invitation]: - """Resends an existing Invitation. - - Args: - invitation_id (str): The unique ID of the Invitation. - - Returns: - Invitation: Invitation response from WorkOS. - """ - ... - - def accept_invitation(self, invitation_id: str) -> SyncOrAsync[Invitation]: - """Accepts an existing Invitation. - - Args: - invitation_id (str): The unique ID of the Invitation. - - Returns: - Invitation: Invitation response from WorkOS. - """ - ... - - def list_feature_flags( - self, - user_id: str, - *, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> SyncOrAsync[FeatureFlagsListResource]: - """Retrieve a list of feature flags for a user - - Args: - user_id (str): User's unique identifier - limit (int): Maximum number of records to return. (Optional) - before (str): Pagination cursor to receive records before a provided Feature Flag ID. (Optional) - after (str): Pagination cursor to receive records after a provided Feature Flag ID. (Optional) - order (Literal["asc","desc"]): Sort records in either ascending or descending (default) order by created_at timestamp. (Optional) - - Returns: - FeatureFlagsListResource: Feature flags list response from WorkOS. - """ - ... - - -class UserManagement(UserManagementModule): - _http_client: SyncHTTPClient - - def __init__( - self, http_client: SyncHTTPClient, client_configuration: ClientConfiguration - ): - self._client_configuration = client_configuration - self._http_client = http_client - - def load_sealed_session( - self, *, sealed_session: str, cookie_password: str - ) -> Session: - return Session( - user_management=self, - client_id=self._http_client.client_id, - session_data=sealed_session, - cookie_password=cookie_password, - jwt_leeway=self._client_configuration.jwt_leeway, - ) - - def get_user(self, user_id: str) -> User: - response = self._http_client.request( - USER_DETAIL_PATH.format(user_id), method=REQUEST_METHOD_GET - ) - - return User.model_validate(response) - - def get_user_by_external_id(self, external_id: str) -> User: - response = self._http_client.request( - USER_DETAIL_BY_EXTERNAL_ID_PATH.format(external_id), - method=REQUEST_METHOD_GET, - ) - - return User.model_validate(response) - - def list_users( - self, - *, - email: Optional[str] = None, - organization_id: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> UsersListResource: - params: UsersListFilters = { - "email": email, - "organization_id": organization_id, - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = self._http_client.request( - USER_PATH, method=REQUEST_METHOD_GET, params=params - ) - - return UsersListResource( - list_method=self.list_users, - list_args=params, - **ListPage[User](**response).model_dump(), - ) - - def create_user( - self, - *, - email: str, - password: Optional[str] = None, - password_hash: Optional[str] = None, - password_hash_type: Optional[PasswordHashType] = None, - first_name: Optional[str] = None, - last_name: Optional[str] = None, - email_verified: Optional[bool] = None, - external_id: Optional[str] = None, - metadata: Optional[Metadata] = None, - ) -> User: - json = { - "email": email, - "password": password, - "password_hash": password_hash, - "password_hash_type": password_hash_type, - "first_name": first_name, - "last_name": last_name, - "email_verified": email_verified or False, - "external_id": external_id, - "metadata": metadata, - } - - response = self._http_client.request( - USER_PATH, method=REQUEST_METHOD_POST, json=json - ) - - return User.model_validate(response) - - def update_user( - self, - *, - user_id: str, - first_name: Optional[str] = None, - last_name: Optional[str] = None, - email: Optional[str] = None, - email_verified: Optional[bool] = None, - password: Optional[str] = None, - password_hash: Optional[str] = None, - password_hash_type: Optional[PasswordHashType] = None, - external_id: Optional[str] = None, - metadata: Optional[Metadata] = None, - locale: Optional[str] = None, - ) -> User: - json = { - "first_name": first_name, - "last_name": last_name, - "email": email, - "email_verified": email_verified, - "password": password, - "password_hash": password_hash, - "password_hash_type": password_hash_type, - "external_id": external_id, - "metadata": metadata, - "locale": locale, - } - - response = self._http_client.request( - USER_DETAIL_PATH.format(user_id), method=REQUEST_METHOD_PUT, json=json - ) - - return User.model_validate(response) - - def delete_user(self, user_id: str) -> None: - self._http_client.request( - USER_DETAIL_PATH.format(user_id), - method=REQUEST_METHOD_DELETE, - ) - - def create_organization_membership( - self, - *, - user_id: str, - organization_id: str, - role_slug: Optional[str] = None, - role_slugs: Optional[Sequence[str]] = None, - ) -> OrganizationMembership: - json = { - "user_id": user_id, - "organization_id": organization_id, - "role_slug": role_slug, - "role_slugs": role_slugs, - } - - response = self._http_client.request( - ORGANIZATION_MEMBERSHIP_PATH, method=REQUEST_METHOD_POST, json=json - ) - - return OrganizationMembership.model_validate(response) - - def update_organization_membership( - self, - *, - organization_membership_id: str, - role_slug: Optional[str] = None, - role_slugs: Optional[Sequence[str]] = None, - ) -> OrganizationMembership: - json = { - "role_slug": role_slug, - "role_slugs": role_slugs, - } - - response = self._http_client.request( - ORGANIZATION_MEMBERSHIP_DETAIL_PATH.format(organization_membership_id), - method=REQUEST_METHOD_PUT, - json=json, - ) - - return OrganizationMembership.model_validate(response) - - def get_organization_membership( - self, organization_membership_id: str - ) -> OrganizationMembership: - response = self._http_client.request( - ORGANIZATION_MEMBERSHIP_DETAIL_PATH.format(organization_membership_id), - method=REQUEST_METHOD_GET, - ) - - return OrganizationMembership.model_validate(response) - - def list_organization_memberships( - self, - *, - user_id: Optional[str] = None, - organization_id: Optional[str] = None, - statuses: Optional[Sequence[OrganizationMembershipStatus]] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> OrganizationMembershipsListResource: - params: OrganizationMembershipsListFilters = { - "user_id": user_id, - "organization_id": organization_id, - "statuses": statuses, - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = self._http_client.request( - ORGANIZATION_MEMBERSHIP_PATH, method=REQUEST_METHOD_GET, params=params - ) - - return OrganizationMembershipsListResource( - list_method=self.list_organization_memberships, - list_args=params, - **ListPage[OrganizationMembership](**response).model_dump(), - ) - - def delete_organization_membership(self, organization_membership_id: str) -> None: - self._http_client.request( - ORGANIZATION_MEMBERSHIP_DETAIL_PATH.format(organization_membership_id), - method=REQUEST_METHOD_DELETE, - ) - - def deactivate_organization_membership( - self, organization_membership_id: str - ) -> OrganizationMembership: - response = self._http_client.request( - ORGANIZATION_MEMBERSHIP_DEACTIVATE_PATH.format(organization_membership_id), - method=REQUEST_METHOD_PUT, - ) - - return OrganizationMembership.model_validate(response) - - def reactivate_organization_membership( - self, organization_membership_id: str - ) -> OrganizationMembership: - response = self._http_client.request( - ORGANIZATION_MEMBERSHIP_REACTIVATE_PATH.format(organization_membership_id), - method=REQUEST_METHOD_PUT, - ) - - return OrganizationMembership.model_validate(response) - - def _authenticate_with( - self, - payload: AuthenticateWithParameters, - response_model: Type[AuthenticationResponseType], - ) -> AuthenticationResponseType: - json = { - "client_id": self._http_client.client_id, - "client_secret": self._http_client.api_key, - **payload, - } - - response = self._http_client.request( - USER_AUTHENTICATE_PATH, - method=REQUEST_METHOD_POST, - json=json, - ) - - response_data = dict(response) - - session = cast(Optional[SessionConfig], payload.get("session", None)) - - if session is not None and session.get("seal_session") is True: - response_data["sealed_session"] = Session.seal_data( - response_data, str(session.get("cookie_password")) - ) - - return response_model.model_validate(response_data) - - def authenticate_with_password( - self, - *, - email: str, - password: str, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - ) -> AuthenticationResponse: - payload: AuthenticateWithPasswordParameters = { - "email": email, - "password": password, - "grant_type": "password", - "ip_address": ip_address, - "user_agent": user_agent, - } - - return self._authenticate_with(payload, response_model=AuthenticationResponse) - - def authenticate_with_code( - self, - *, - code: str, - session: Optional[SessionConfig] = None, - code_verifier: Optional[str] = None, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - invitation_token: Optional[str] = None, - ) -> AuthKitAuthenticationResponse: - if ( - session is not None - and session.get("seal_session") - and not session.get("cookie_password") - ): - raise ValueError("cookie_password is required when sealing session") - - payload: AuthenticateWithCodeParameters = { - "code": code, - "grant_type": "authorization_code", - "ip_address": ip_address, - "user_agent": user_agent, - "code_verifier": code_verifier, - "session": session, - "invitation_token": invitation_token, - } - - return self._authenticate_with( - payload, response_model=AuthKitAuthenticationResponse - ) - - def authenticate_with_magic_auth( - self, - *, - code: str, - email: str, - link_authorization_code: Optional[str] = None, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - ) -> AuthenticationResponse: - payload: AuthenticateWithMagicAuthParameters = { - "code": code, - "email": email, - "grant_type": "urn:workos:oauth:grant-type:magic-auth:code", - "link_authorization_code": link_authorization_code, - "ip_address": ip_address, - "user_agent": user_agent, - } - - return self._authenticate_with(payload, response_model=AuthenticationResponse) - - def authenticate_with_email_verification( - self, - *, - code: str, - pending_authentication_token: str, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - ) -> AuthenticationResponse: - payload: AuthenticateWithEmailVerificationParameters = { - "code": code, - "pending_authentication_token": pending_authentication_token, - "grant_type": "urn:workos:oauth:grant-type:email-verification:code", - "ip_address": ip_address, - "user_agent": user_agent, - } - - return self._authenticate_with(payload, response_model=AuthenticationResponse) - - def authenticate_with_totp( - self, - *, - code: str, - authentication_challenge_id: str, - pending_authentication_token: str, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - ) -> AuthenticationResponse: - payload: AuthenticateWithTotpParameters = { - "code": code, - "authentication_challenge_id": authentication_challenge_id, - "pending_authentication_token": pending_authentication_token, - "grant_type": "urn:workos:oauth:grant-type:mfa-totp", - "ip_address": ip_address, - "user_agent": user_agent, - } - - return self._authenticate_with(payload, response_model=AuthenticationResponse) - - def authenticate_with_organization_selection( - self, - *, - organization_id: str, - pending_authentication_token: str, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - ) -> AuthenticationResponse: - payload: AuthenticateWithOrganizationSelectionParameters = { - "organization_id": organization_id, - "pending_authentication_token": pending_authentication_token, - "grant_type": "urn:workos:oauth:grant-type:organization-selection", - "ip_address": ip_address, - "user_agent": user_agent, - } - - return self._authenticate_with(payload, response_model=AuthenticationResponse) - - def authenticate_with_refresh_token( - self, - *, - refresh_token: str, - session: Optional[SessionConfig] = None, - organization_id: Optional[str] = None, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - ) -> RefreshTokenAuthenticationResponse: - if ( - session is not None - and session.get("seal_session") - and not session.get("cookie_password") - ): - raise ValueError("cookie_password is required when sealing session") - - payload: AuthenticateWithRefreshTokenParameters = { - "refresh_token": refresh_token, - "organization_id": organization_id, - "grant_type": "refresh_token", - "ip_address": ip_address, - "user_agent": user_agent, - "session": session, - } - - return self._authenticate_with( - payload, response_model=RefreshTokenAuthenticationResponse - ) - - def get_password_reset(self, password_reset_id: str) -> PasswordReset: - response = self._http_client.request( - PASSWORD_RESET_DETAIL_PATH.format(password_reset_id), - method=REQUEST_METHOD_GET, - ) - - return PasswordReset.model_validate(response) - - def create_password_reset(self, email: str) -> PasswordReset: - json = { - "email": email, - } - - response = self._http_client.request( - PASSWORD_RESET_PATH, method=REQUEST_METHOD_POST, json=json - ) - - return PasswordReset.model_validate(response) - - def reset_password(self, *, token: str, new_password: str) -> User: - json = { - "token": token, - "new_password": new_password, - } - - response = self._http_client.request( - USER_RESET_PASSWORD_PATH, method=REQUEST_METHOD_POST, json=json - ) - - return User.model_validate(response["user"]) - - def get_email_verification(self, email_verification_id: str) -> EmailVerification: - response = self._http_client.request( - EMAIL_VERIFICATION_DETAIL_PATH.format(email_verification_id), - method=REQUEST_METHOD_GET, - ) - - return EmailVerification.model_validate(response) - - def send_verification_email(self, user_id: str) -> User: - response = self._http_client.request( - USER_SEND_VERIFICATION_EMAIL_PATH.format(user_id), - method=REQUEST_METHOD_POST, - ) - - return User.model_validate(response["user"]) - - def verify_email(self, *, user_id: str, code: str) -> User: - json = { - "code": code, - } - - response = self._http_client.request( - USER_VERIFY_EMAIL_CODE_PATH.format(user_id), - method=REQUEST_METHOD_POST, - json=json, - ) - - return User.model_validate(response["user"]) - - def get_magic_auth(self, magic_auth_id: str) -> MagicAuth: - response = self._http_client.request( - MAGIC_AUTH_DETAIL_PATH.format(magic_auth_id), method=REQUEST_METHOD_GET - ) - - return MagicAuth.model_validate(response) - - def create_magic_auth( - self, *, email: str, invitation_token: Optional[str] = None - ) -> MagicAuth: - json = { - "email": email, - "invitation_token": invitation_token, - } - - response = self._http_client.request( - MAGIC_AUTH_PATH, method=REQUEST_METHOD_POST, json=json - ) - - return MagicAuth.model_validate(response) - - def list_sessions( - self, - *, - user_id: str, - limit: Optional[int] = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: Optional[PaginationOrder] = "desc", - ) -> "SessionsListResource": - limit_value: int = limit if limit is not None else DEFAULT_LIST_RESPONSE_LIMIT - - params: ListArgs = { - "limit": limit_value, - "before": before, - "after": after, - "order": order, - } - - response = self._http_client.request( - USER_SESSIONS_PATH.format(user_id), - method=REQUEST_METHOD_GET, - params=params, - ) - - list_args: SessionsListFilters = { - "limit": limit_value, - "before": before, - "after": after, - "user_id": user_id, - } - if order is not None: - list_args["order"] = order - - return SessionsListResource( - list_method=self.list_sessions, - list_args=list_args, - **ListPage[UserManagementSession](**response).model_dump(), - ) - - def revoke_session(self, *, session_id: str) -> None: - json = {"session_id": session_id} - - self._http_client.request( - SESSIONS_REVOKE_PATH, method=REQUEST_METHOD_POST, json=json - ) - - def enroll_auth_factor( - self, - *, - user_id: str, - type: AuthenticationFactorType, - totp_issuer: Optional[str] = None, - totp_user: Optional[str] = None, - totp_secret: Optional[str] = None, - ) -> AuthenticationFactorTotpAndChallengeResponse: - json = { - "type": type, - "totp_issuer": totp_issuer, - "totp_user": totp_user, - "totp_secret": totp_secret, - } - - response = self._http_client.request( - USER_AUTH_FACTORS_PATH.format(user_id), - method=REQUEST_METHOD_POST, - json=json, - ) - - return AuthenticationFactorTotpAndChallengeResponse.model_validate(response) - - def list_auth_factors( - self, - *, - user_id: str, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> AuthenticationFactorsListResource: - params: ListArgs = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = self._http_client.request( - USER_AUTH_FACTORS_PATH.format(user_id), - method=REQUEST_METHOD_GET, - params=params, - ) - - # We don't spread params on this dict to make mypy happy - list_args: AuthenticationFactorsListFilters = { - "limit": limit or DEFAULT_LIST_RESPONSE_LIMIT, - "before": before, - "after": after, - "order": order, - "user_id": user_id, - } - - return AuthenticationFactorsListResource( - list_method=self.list_auth_factors, - list_args=list_args, - **ListPage[AuthenticationFactor](**response).model_dump(), - ) - - def get_invitation(self, invitation_id: str) -> Invitation: - response = self._http_client.request( - INVITATION_DETAIL_PATH.format(invitation_id), - method=REQUEST_METHOD_GET, - ) - - return Invitation.model_validate(response) - - def find_invitation_by_token(self, invitation_token: str) -> Invitation: - response = self._http_client.request( - INVITATION_DETAIL_BY_TOKEN_PATH.format(invitation_token), - method=REQUEST_METHOD_GET, - ) - - return Invitation.model_validate(response) - - def list_invitations( - self, - *, - email: Optional[str] = None, - organization_id: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> InvitationsListResource: - params: InvitationsListFilters = { - "email": email, - "organization_id": organization_id, - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = self._http_client.request( - INVITATION_PATH, method=REQUEST_METHOD_GET, params=params - ) - - return InvitationsListResource( - list_method=self.list_invitations, - list_args=params, - **ListPage[Invitation](**response).model_dump(), - ) - - def send_invitation( - self, - *, - email: str, - organization_id: Optional[str] = None, - expires_in_days: Optional[int] = None, - inviter_user_id: Optional[str] = None, - role_slug: Optional[str] = None, - ) -> Invitation: - json = { - "email": email, - "organization_id": organization_id, - "expires_in_days": expires_in_days, - "inviter_user_id": inviter_user_id, - "role_slug": role_slug, - } - - response = self._http_client.request( - INVITATION_PATH, method=REQUEST_METHOD_POST, json=json - ) - - return Invitation.model_validate(response) - - def revoke_invitation(self, invitation_id: str) -> Invitation: - response = self._http_client.request( - INVITATION_REVOKE_PATH.format(invitation_id), method=REQUEST_METHOD_POST - ) - - return Invitation.model_validate(response) - - def resend_invitation(self, invitation_id: str) -> Invitation: - response = self._http_client.request( - INVITATION_RESEND_PATH.format(invitation_id), method=REQUEST_METHOD_POST - ) - - return Invitation.model_validate(response) - - def accept_invitation(self, invitation_id: str) -> Invitation: - response = self._http_client.request( - INVITATION_ACCEPT_PATH.format(invitation_id), method=REQUEST_METHOD_POST - ) - - return Invitation.model_validate(response) - - def list_feature_flags( - self, - user_id: str, - *, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> FeatureFlagsListResource: - list_params: FeatureFlagListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = self._http_client.request( - USER_FEATURE_FLAGS_PATH.format(user_id), - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return FeatureFlagsListResource( - list_method=self.list_feature_flags, - list_args=list_params, - **ListPage[FeatureFlag](**response).model_dump(), - ) - - -class AsyncUserManagement(UserManagementModule): - _http_client: AsyncHTTPClient - - def __init__( - self, http_client: AsyncHTTPClient, client_configuration: ClientConfiguration - ): - self._client_configuration = client_configuration - self._http_client = http_client - - async def load_sealed_session( - self, *, sealed_session: str, cookie_password: str - ) -> AsyncSession: - return AsyncSession( - user_management=self, - client_id=self._http_client.client_id, - session_data=sealed_session, - cookie_password=cookie_password, - jwt_leeway=self._client_configuration.jwt_leeway, - ) - - async def get_user(self, user_id: str) -> User: - response = await self._http_client.request( - USER_DETAIL_PATH.format(user_id), method=REQUEST_METHOD_GET - ) - - return User.model_validate(response) - - async def get_user_by_external_id(self, external_id: str) -> User: - response = await self._http_client.request( - USER_DETAIL_BY_EXTERNAL_ID_PATH.format(external_id), - method=REQUEST_METHOD_GET, - ) - - return User.model_validate(response) - - async def list_users( - self, - *, - email: Optional[str] = None, - organization_id: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> UsersListResource: - params: UsersListFilters = { - "email": email, - "organization_id": organization_id, - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = await self._http_client.request( - USER_PATH, method=REQUEST_METHOD_GET, params=params - ) - - return UsersListResource( - list_method=self.list_users, - list_args=params, - **ListPage[User](**response).model_dump(), - ) - - async def create_user( - self, - *, - email: str, - password: Optional[str] = None, - password_hash: Optional[str] = None, - password_hash_type: Optional[PasswordHashType] = None, - first_name: Optional[str] = None, - last_name: Optional[str] = None, - email_verified: Optional[bool] = None, - external_id: Optional[str] = None, - metadata: Optional[Metadata] = None, - ) -> User: - json = { - "email": email, - "password": password, - "password_hash": password_hash, - "password_hash_type": password_hash_type, - "first_name": first_name, - "last_name": last_name, - "email_verified": email_verified or False, - "external_id": external_id, - "metadata": metadata, - } - - response = await self._http_client.request( - USER_PATH, method=REQUEST_METHOD_POST, json=json - ) - - return User.model_validate(response) - - async def update_user( - self, - *, - user_id: str, - first_name: Optional[str] = None, - last_name: Optional[str] = None, - email: Optional[str] = None, - email_verified: Optional[bool] = None, - password: Optional[str] = None, - password_hash: Optional[str] = None, - password_hash_type: Optional[PasswordHashType] = None, - external_id: Optional[str] = None, - metadata: Optional[Metadata] = None, - locale: Optional[str] = None, - ) -> User: - json = { - "first_name": first_name, - "last_name": last_name, - "email": email, - "email_verified": email_verified, - "password": password, - "password_hash": password_hash, - "password_hash_type": password_hash_type, - "external_id": external_id, - "metadata": metadata, - "locale": locale, - } - - response = await self._http_client.request( - USER_DETAIL_PATH.format(user_id), method=REQUEST_METHOD_PUT, json=json - ) - - return User.model_validate(response) - - async def delete_user(self, user_id: str) -> None: - await self._http_client.request( - USER_DETAIL_PATH.format(user_id), method=REQUEST_METHOD_DELETE - ) - - async def create_organization_membership( - self, - *, - user_id: str, - organization_id: str, - role_slug: Optional[str] = None, - role_slugs: Optional[Sequence[str]] = None, - ) -> OrganizationMembership: - json = { - "user_id": user_id, - "organization_id": organization_id, - "role_slug": role_slug, - "role_slugs": role_slugs, - } - - response = await self._http_client.request( - ORGANIZATION_MEMBERSHIP_PATH, method=REQUEST_METHOD_POST, json=json - ) - - return OrganizationMembership.model_validate(response) - - async def update_organization_membership( - self, - *, - organization_membership_id: str, - role_slug: Optional[str] = None, - role_slugs: Optional[Sequence[str]] = None, - ) -> OrganizationMembership: - json = { - "role_slug": role_slug, - "role_slugs": role_slugs, - } - - response = await self._http_client.request( - ORGANIZATION_MEMBERSHIP_DETAIL_PATH.format(organization_membership_id), - method=REQUEST_METHOD_PUT, - json=json, - ) - - return OrganizationMembership.model_validate(response) - - async def get_organization_membership( - self, organization_membership_id: str - ) -> OrganizationMembership: - response = await self._http_client.request( - ORGANIZATION_MEMBERSHIP_DETAIL_PATH.format(organization_membership_id), - method=REQUEST_METHOD_GET, - ) - - return OrganizationMembership.model_validate(response) - - async def list_organization_memberships( - self, - *, - user_id: Optional[str] = None, - organization_id: Optional[str] = None, - statuses: Optional[Sequence[OrganizationMembershipStatus]] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> OrganizationMembershipsListResource: - params: OrganizationMembershipsListFilters = { - "user_id": user_id, - "organization_id": organization_id, - "statuses": statuses, - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = await self._http_client.request( - ORGANIZATION_MEMBERSHIP_PATH, method=REQUEST_METHOD_GET, params=params - ) - - return OrganizationMembershipsListResource( - list_method=self.list_organization_memberships, - list_args=params, - **ListPage[OrganizationMembership](**response).model_dump(), - ) - - async def delete_organization_membership( - self, organization_membership_id: str - ) -> None: - await self._http_client.request( - ORGANIZATION_MEMBERSHIP_DETAIL_PATH.format(organization_membership_id), - method=REQUEST_METHOD_DELETE, - ) - - async def deactivate_organization_membership( - self, organization_membership_id: str - ) -> OrganizationMembership: - response = await self._http_client.request( - ORGANIZATION_MEMBERSHIP_DEACTIVATE_PATH.format(organization_membership_id), - method=REQUEST_METHOD_PUT, - ) - - return OrganizationMembership.model_validate(response) - - async def reactivate_organization_membership( - self, organization_membership_id: str - ) -> OrganizationMembership: - response = await self._http_client.request( - ORGANIZATION_MEMBERSHIP_REACTIVATE_PATH.format(organization_membership_id), - method=REQUEST_METHOD_PUT, - ) - - return OrganizationMembership.model_validate(response) - - async def _authenticate_with( - self, - payload: AuthenticateWithParameters, - response_model: Type[AuthenticationResponseType], - ) -> AuthenticationResponseType: - json = { - "client_id": self._http_client.client_id, - "client_secret": self._http_client.api_key, - **payload, - } - - response = await self._http_client.request( - USER_AUTHENTICATE_PATH, - method=REQUEST_METHOD_POST, - json=json, - ) - - response_data = dict(response) - - session = cast(Optional[SessionConfig], payload.get("session", None)) - - if session is not None and session.get("seal_session") is True: - response_data["sealed_session"] = Session.seal_data( - response_data, str(session.get("cookie_password")) - ) - - return response_model.model_validate(response_data) - - async def authenticate_with_password( - self, - *, - email: str, - password: str, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - ) -> AuthenticationResponse: - payload: AuthenticateWithPasswordParameters = { - "email": email, - "password": password, - "grant_type": "password", - "ip_address": ip_address, - "user_agent": user_agent, - } - - return await self._authenticate_with( - payload, response_model=AuthenticationResponse - ) - - async def authenticate_with_code( - self, - *, - code: str, - session: Optional[SessionConfig] = None, - code_verifier: Optional[str] = None, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - invitation_token: Optional[str] = None, - ) -> AuthKitAuthenticationResponse: - if ( - session is not None - and session.get("seal_session") - and not session.get("cookie_password") - ): - raise ValueError("cookie_password is required when sealing session") - - payload: AuthenticateWithCodeParameters = { - "code": code, - "grant_type": "authorization_code", - "ip_address": ip_address, - "user_agent": user_agent, - "code_verifier": code_verifier, - "session": session, - "invitation_token": invitation_token, - } - - return await self._authenticate_with( - payload, response_model=AuthKitAuthenticationResponse - ) - - async def authenticate_with_magic_auth( - self, - *, - code: str, - email: str, - link_authorization_code: Optional[str] = None, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - ) -> AuthenticationResponse: - payload: AuthenticateWithMagicAuthParameters = { - "code": code, - "email": email, - "grant_type": "urn:workos:oauth:grant-type:magic-auth:code", - "link_authorization_code": link_authorization_code, - "ip_address": ip_address, - "user_agent": user_agent, - } - - return await self._authenticate_with( - payload, response_model=AuthenticationResponse - ) - - async def authenticate_with_email_verification( - self, - *, - code: str, - pending_authentication_token: str, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - ) -> AuthenticationResponse: - payload: AuthenticateWithEmailVerificationParameters = { - "code": code, - "pending_authentication_token": pending_authentication_token, - "grant_type": "urn:workos:oauth:grant-type:email-verification:code", - "ip_address": ip_address, - "user_agent": user_agent, - } - - return await self._authenticate_with( - payload, response_model=AuthenticationResponse - ) - - async def authenticate_with_totp( - self, - *, - code: str, - authentication_challenge_id: str, - pending_authentication_token: str, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - ) -> AuthenticationResponse: - payload: AuthenticateWithTotpParameters = { - "code": code, - "authentication_challenge_id": authentication_challenge_id, - "pending_authentication_token": pending_authentication_token, - "grant_type": "urn:workos:oauth:grant-type:mfa-totp", - "ip_address": ip_address, - "user_agent": user_agent, - } - - return await self._authenticate_with( - payload, response_model=AuthenticationResponse - ) - - async def authenticate_with_organization_selection( - self, - *, - organization_id: str, - pending_authentication_token: str, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - ) -> AuthenticationResponse: - payload: AuthenticateWithOrganizationSelectionParameters = { - "organization_id": organization_id, - "pending_authentication_token": pending_authentication_token, - "grant_type": "urn:workos:oauth:grant-type:organization-selection", - "ip_address": ip_address, - "user_agent": user_agent, - } - - return await self._authenticate_with( - payload, response_model=AuthenticationResponse - ) - - async def authenticate_with_refresh_token( - self, - *, - refresh_token: str, - session: Optional[SessionConfig] = None, - organization_id: Optional[str] = None, - ip_address: Optional[str] = None, - user_agent: Optional[str] = None, - ) -> RefreshTokenAuthenticationResponse: - if ( - session is not None - and session.get("seal_session") - and not session.get("cookie_password") - ): - raise ValueError("cookie_password is required when sealing session") - - payload: AuthenticateWithRefreshTokenParameters = { - "refresh_token": refresh_token, - "organization_id": organization_id, - "grant_type": "refresh_token", - "ip_address": ip_address, - "user_agent": user_agent, - "session": session, - } - - return await self._authenticate_with( - payload, response_model=RefreshTokenAuthenticationResponse - ) - - async def get_password_reset(self, password_reset_id: str) -> PasswordReset: - response = await self._http_client.request( - PASSWORD_RESET_DETAIL_PATH.format(password_reset_id), - method=REQUEST_METHOD_GET, - ) - - return PasswordReset.model_validate(response) - - async def create_password_reset(self, email: str) -> PasswordReset: - json = { - "email": email, - } - - response = await self._http_client.request( - PASSWORD_RESET_PATH, method=REQUEST_METHOD_POST, json=json - ) - - return PasswordReset.model_validate(response) - - async def reset_password(self, *, token: str, new_password: str) -> User: - json = { - "token": token, - "new_password": new_password, - } - - response = await self._http_client.request( - USER_RESET_PASSWORD_PATH, method=REQUEST_METHOD_POST, json=json - ) - - return User.model_validate(response["user"]) - - async def get_email_verification( - self, email_verification_id: str - ) -> EmailVerification: - response = await self._http_client.request( - EMAIL_VERIFICATION_DETAIL_PATH.format(email_verification_id), - method=REQUEST_METHOD_GET, - ) - - return EmailVerification.model_validate(response) - - async def send_verification_email(self, user_id: str) -> User: - response = await self._http_client.request( - USER_SEND_VERIFICATION_EMAIL_PATH.format(user_id), - method=REQUEST_METHOD_POST, - ) - - return User.model_validate(response["user"]) - - async def verify_email(self, *, user_id: str, code: str) -> User: - json = { - "code": code, - } - - response = await self._http_client.request( - USER_VERIFY_EMAIL_CODE_PATH.format(user_id), - method=REQUEST_METHOD_POST, - json=json, - ) - - return User.model_validate(response["user"]) - - async def get_magic_auth(self, magic_auth_id: str) -> MagicAuth: - response = await self._http_client.request( - MAGIC_AUTH_DETAIL_PATH.format(magic_auth_id), method=REQUEST_METHOD_GET - ) - - return MagicAuth.model_validate(response) - - async def create_magic_auth( - self, - *, - email: str, - invitation_token: Optional[str] = None, - ) -> MagicAuth: - json = { - "email": email, - "invitation_token": invitation_token, - } - - response = await self._http_client.request( - MAGIC_AUTH_PATH, method=REQUEST_METHOD_POST, json=json - ) - - return MagicAuth.model_validate(response) - - async def list_sessions( - self, - *, - user_id: str, - limit: Optional[int] = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: Optional[PaginationOrder] = "desc", - ) -> "SessionsListResource": - limit_value: int = limit if limit is not None else DEFAULT_LIST_RESPONSE_LIMIT - - params: ListArgs = { - "limit": limit_value, - "before": before, - "after": after, - "order": order, - } - - response = await self._http_client.request( - USER_SESSIONS_PATH.format(user_id), - method=REQUEST_METHOD_GET, - params=params, - ) - - list_args: SessionsListFilters = { - "limit": limit_value, - "before": before, - "after": after, - "user_id": user_id, - } - if order is not None: - list_args["order"] = order - - return SessionsListResource( - list_method=self.list_sessions, - list_args=list_args, - **ListPage[UserManagementSession](**response).model_dump(), - ) - - async def revoke_session(self, *, session_id: str) -> None: - json = {"session_id": session_id} - - await self._http_client.request( - SESSIONS_REVOKE_PATH, method=REQUEST_METHOD_POST, json=json - ) - - async def enroll_auth_factor( - self, - *, - user_id: str, - type: AuthenticationFactorType, - totp_issuer: Optional[str] = None, - totp_user: Optional[str] = None, - totp_secret: Optional[str] = None, - ) -> AuthenticationFactorTotpAndChallengeResponse: - json = { - "type": type, - "totp_issuer": totp_issuer, - "totp_user": totp_user, - "totp_secret": totp_secret, - } - - response = await self._http_client.request( - USER_AUTH_FACTORS_PATH.format(user_id), - method=REQUEST_METHOD_POST, - json=json, - ) - - return AuthenticationFactorTotpAndChallengeResponse.model_validate(response) - - async def list_auth_factors( - self, - *, - user_id: str, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> AuthenticationFactorsListResource: - params: ListArgs = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = await self._http_client.request( - USER_AUTH_FACTORS_PATH.format(user_id), - method=REQUEST_METHOD_GET, - params=params, - ) - - # We don't spread params on this dict to make mypy happy - list_args: AuthenticationFactorsListFilters = { - "limit": limit or DEFAULT_LIST_RESPONSE_LIMIT, - "before": before, - "after": after, - "order": order, - "user_id": user_id, - } - - return AuthenticationFactorsListResource( - list_method=self.list_auth_factors, - list_args=list_args, - **ListPage[AuthenticationFactor](**response).model_dump(), - ) - - async def get_invitation(self, invitation_id: str) -> Invitation: - response = await self._http_client.request( - INVITATION_DETAIL_PATH.format(invitation_id), method=REQUEST_METHOD_GET - ) - - return Invitation.model_validate(response) - - async def find_invitation_by_token(self, invitation_token: str) -> Invitation: - response = await self._http_client.request( - INVITATION_DETAIL_BY_TOKEN_PATH.format(invitation_token), - method=REQUEST_METHOD_GET, - ) - - return Invitation.model_validate(response) - - async def list_invitations( - self, - *, - email: Optional[str] = None, - organization_id: Optional[str] = None, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> InvitationsListResource: - params: InvitationsListFilters = { - "email": email, - "organization_id": organization_id, - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = await self._http_client.request( - INVITATION_PATH, method=REQUEST_METHOD_GET, params=params - ) - - return InvitationsListResource( - list_method=self.list_invitations, - list_args=params, - **ListPage[Invitation](**response).model_dump(), - ) - - async def send_invitation( - self, - *, - email: str, - organization_id: Optional[str] = None, - expires_in_days: Optional[int] = None, - inviter_user_id: Optional[str] = None, - role_slug: Optional[str] = None, - ) -> Invitation: - json = { - "email": email, - "organization_id": organization_id, - "expires_in_days": expires_in_days, - "inviter_user_id": inviter_user_id, - "role_slug": role_slug, - } - - response = await self._http_client.request( - INVITATION_PATH, method=REQUEST_METHOD_POST, json=json - ) - - return Invitation.model_validate(response) - - async def revoke_invitation(self, invitation_id: str) -> Invitation: - response = await self._http_client.request( - INVITATION_REVOKE_PATH.format(invitation_id), method=REQUEST_METHOD_POST - ) - - return Invitation.model_validate(response) - - async def resend_invitation(self, invitation_id: str) -> Invitation: - response = await self._http_client.request( - INVITATION_RESEND_PATH.format(invitation_id), method=REQUEST_METHOD_POST - ) - - return Invitation.model_validate(response) - - async def accept_invitation(self, invitation_id: str) -> Invitation: - response = await self._http_client.request( - INVITATION_ACCEPT_PATH.format(invitation_id), method=REQUEST_METHOD_POST - ) - - return Invitation.model_validate(response) - - async def list_feature_flags( - self, - user_id: str, - *, - limit: int = DEFAULT_LIST_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - order: PaginationOrder = "desc", - ) -> FeatureFlagsListResource: - list_params: FeatureFlagListFilters = { - "limit": limit, - "before": before, - "after": after, - "order": order, - } - - response = await self._http_client.request( - USER_FEATURE_FLAGS_PATH.format(user_id), - method=REQUEST_METHOD_GET, - params=list_params, - ) - - return FeatureFlagsListResource( - list_method=self.list_feature_flags, - list_args=list_params, - **ListPage[FeatureFlag](**response).model_dump(), - ) diff --git a/src/workos/utils/__init__.py b/src/workos/utils/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/workos/utils/_base_http_client.py b/src/workos/utils/_base_http_client.py deleted file mode 100644 index 3bcddf32..00000000 --- a/src/workos/utils/_base_http_client.py +++ /dev/null @@ -1,255 +0,0 @@ -import platform -from typing import ( - Any, - Mapping, - Sequence, - cast, - Dict, - Generic, - Optional, - TypeVar, - Union, -) -from typing_extensions import NotRequired, TypedDict - -import httpx -from httpx._types import QueryParamTypes - -from workos.exceptions import ( - ConflictException, - ServerException, - AuthenticationException, - AuthorizationException, - EmailVerificationRequiredException, - NotFoundException, - BadRequestException, -) -from workos.utils.request_helper import REQUEST_METHOD_DELETE, REQUEST_METHOD_GET - - -_HttpxClientT = TypeVar("_HttpxClientT", bound=Union[httpx.Client, httpx.AsyncClient]) - - -DEFAULT_REQUEST_TIMEOUT = 25 - - -ParamsType = Optional[Mapping[str, Any]] -HeadersType = Optional[Dict[str, str]] -JsonType = Optional[Union[Mapping[str, Any], Sequence[Any]]] -ResponseJson = Mapping[Any, Any] - - -class PreparedRequest(TypedDict): - method: str - url: str - headers: httpx.Headers - params: NotRequired[Optional[QueryParamTypes]] - json: NotRequired[JsonType] - timeout: int - - -class BaseHTTPClient(Generic[_HttpxClientT]): - _client: _HttpxClientT - - _api_key: str - _client_id: str - _base_url: str - _version: str - _timeout: int - - def __init__( - self, - *, - api_key: str, - base_url: str, - client_id: str, - version: str, - timeout: Optional[int] = DEFAULT_REQUEST_TIMEOUT, - ) -> None: - self._api_key = api_key - self._base_url = base_url - self._client_id = client_id - self._version = version - self._timeout = DEFAULT_REQUEST_TIMEOUT if timeout is None else timeout - - def _generate_api_url(self, path: str) -> str: - return f"{self._base_url}{path}" - - def _build_headers( - self, - *, - custom_headers: Union[HeadersType, None], - exclude_default_auth_headers: bool = False, - ) -> httpx.Headers: - if custom_headers is None: - custom_headers = {} - - default_headers = { - **self.default_headers, - **({} if exclude_default_auth_headers else self.auth_headers), - } - - # httpx.Headers is case-insensitive while dictionaries are not. - return httpx.Headers({**default_headers, **custom_headers}) - - def _maybe_raise_error_by_status_code( - self, response: httpx.Response, response_json: Union[ResponseJson, None] - ) -> None: - status_code = response.status_code - if status_code >= 400 and status_code < 500: - if status_code == 401: - raise AuthenticationException(response, response_json) - elif status_code == 403: - if ( - response_json is not None - and response_json.get("code") == "email_verification_required" - ): - raise EmailVerificationRequiredException(response, response_json) - raise AuthorizationException(response, response_json) - elif status_code == 404: - raise NotFoundException(response, response_json) - elif status_code == 409: - raise ConflictException(response, response_json) - - raise BadRequestException(response, response_json) - elif status_code >= 500 and status_code < 600: - raise ServerException(response, response_json) - - def _prepare_request( - self, - path: str, - method: Optional[str] = REQUEST_METHOD_GET, - params: ParamsType = None, - json: JsonType = None, - headers: HeadersType = None, - exclude_default_auth_headers: bool = False, - force_include_body: bool = False, - exclude_none: bool = True, - ) -> PreparedRequest: - """Executes a request against the WorkOS API. - - Args: - path (str): Path for the api request that'd be appended to the base API URL - - Kwargs: - method Optional[str]: One of the supported methods as defined by the REQUEST_METHOD_X constants - params Optional[dict]: Query params or body payload to be added to the request - headers Optional[dict]: Custom headers to be added to the request - exclude_default_auth_headers (bool): If True, excludes default auth headers from the request - force_include_body (bool): If True, allows sending a body in a bodyless request (used for DELETE requests) - exclude_none (bool): If True (default), strips keys with None values from the JSON body so only defined fields are sent. - Returns: - dict: Response from WorkOS - """ - url = self._generate_api_url(path) - parsed_headers = self._build_headers( - custom_headers=headers, - exclude_default_auth_headers=exclude_default_auth_headers, - ) - parsed_method = REQUEST_METHOD_GET if method is None else method - bodyless_http_method = parsed_method.lower() in [ - REQUEST_METHOD_DELETE, - REQUEST_METHOD_GET, - ] - - if bodyless_http_method and json is not None and not force_include_body: - raise ValueError(f"Cannot send a body with a {parsed_method} request") - - # Remove any parameters that are None - if params is not None: - params = {k: v for k, v in params.items() if v is not None} - - # Remove any body values that are None - if exclude_none and json is not None and isinstance(json, Mapping): - json = {k: v for k, v in json.items() if v is not None} - - # We'll spread these return values onto the HTTP client request method - if bodyless_http_method and not force_include_body: - return { - "method": parsed_method, - "url": url, - "headers": parsed_headers, - "params": params, - "timeout": self.timeout, - } - else: - return { - "method": parsed_method, - "url": url, - "headers": parsed_headers, - "params": params, - "json": json, - "timeout": self.timeout, - } - - def _handle_response(self, response: httpx.Response) -> ResponseJson: - response_json = None - content_type = ( - response.headers.get("content-type") - if response.headers is not None - else None - ) - if content_type is not None and "application/json" in content_type: - try: - response_json = response.json() - except ValueError: - raise ServerException(response, None) - - self._maybe_raise_error_by_status_code(response, response_json) - - return cast(ResponseJson, response_json) - - def build_request_url( - self, - url: str, - method: Optional[str] = REQUEST_METHOD_GET, - params: Optional[QueryParamTypes] = None, - ) -> str: - return self._client.build_request( - method=method or REQUEST_METHOD_GET, url=url, params=params - ).url.__str__() - - @property - def api_key(self) -> str: - return self._api_key - - @property - def base_url(self) -> str: - return self._base_url - - @property - def client_id(self) -> str: - return self._client_id - - @property - def auth_headers(self) -> Mapping[str, str]: - return self.auth_header_from_token(self._api_key) - - def auth_header_from_token(self, token: str) -> Mapping[str, str]: - return { - "Authorization": f"Bearer {token}", - } - - @property - def default_headers(self) -> Dict[str, str]: - return { - "Accept": "application/json", - "Content-Type": "application/json", - "User-Agent": self.user_agent, - } - - @property - def user_agent(self) -> str: - # TODO: Include sync/async in user agent - return "WorkOS Python/{} Python SDK/{}".format( - platform.python_version(), - self._version, - ) - - @property - def timeout(self) -> int: - return self._timeout - - @property - def version(self) -> str: - return self._version diff --git a/src/workos/utils/crypto_provider.py b/src/workos/utils/crypto_provider.py deleted file mode 100644 index 1cb84241..00000000 --- a/src/workos/utils/crypto_provider.py +++ /dev/null @@ -1,39 +0,0 @@ -import os -from typing import Optional, Dict -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes -from cryptography.hazmat.backends import default_backend - - -class CryptoProvider: - def encrypt( - self, plaintext: bytes, key: bytes, iv: bytes, aad: Optional[bytes] - ) -> Dict[str, bytes]: - encryptor = Cipher( - algorithms.AES(key), modes.GCM(iv), backend=default_backend() - ).encryptor() - - if aad: - encryptor.authenticate_additional_data(aad) - - ciphertext = encryptor.update(plaintext) + encryptor.finalize() - return {"ciphertext": ciphertext, "iv": iv, "tag": encryptor.tag} - - def decrypt( - self, - ciphertext: bytes, - key: bytes, - iv: bytes, - tag: bytes, - aad: Optional[bytes] = None, - ) -> bytes: - decryptor = Cipher( - algorithms.AES(key), modes.GCM(iv, tag), backend=default_backend() - ).decryptor() - - if aad: - decryptor.authenticate_additional_data(aad) - - return decryptor.update(ciphertext) + decryptor.finalize() - - def random_bytes(self, n: int) -> bytes: - return os.urandom(n) diff --git a/src/workos/utils/http_client.py b/src/workos/utils/http_client.py deleted file mode 100644 index 5c7deac5..00000000 --- a/src/workos/utils/http_client.py +++ /dev/null @@ -1,262 +0,0 @@ -import asyncio -from types import TracebackType -from typing import Optional, Type, Union - -# Self was added to typing in Python 3.11 -from typing_extensions import Self - -import httpx - -from workos.utils._base_http_client import ( - BaseHTTPClient, - HeadersType, - JsonType, - ParamsType, - ResponseJson, -) -from workos.utils.request_helper import REQUEST_METHOD_DELETE, REQUEST_METHOD_GET - - -class SyncHttpxClientWrapper(httpx.Client): - def __del__(self) -> None: - try: - self.close() - except Exception: - pass - - -class SyncHTTPClient(BaseHTTPClient[httpx.Client]): - """Sync HTTP Client for a convenient way to access the WorkOS feature set.""" - - _client: httpx.Client - - def __init__( - self, - *, - api_key: str, - base_url: str, - client_id: str, - version: str, - timeout: Optional[int] = None, - # If no custom transport is provided, let httpx use the default - # so we don't overwrite environment configurations like proxies - transport: Optional[httpx.BaseTransport] = None, - ) -> None: - super().__init__( - api_key=api_key, - base_url=base_url, - client_id=client_id, - version=version, - timeout=timeout, - ) - self._client = SyncHttpxClientWrapper( - base_url=base_url, - timeout=timeout, - follow_redirects=True, - transport=transport, - ) - - def is_closed(self) -> bool: - return self._client.is_closed - - def close(self) -> None: - """Close the underlying HTTPX client. - - The client will *not* be usable after this. - """ - # If an error is thrown while constructing a client, self._client - # may not be present - if hasattr(self, "_client"): - self._client.close() - - def __enter__(self) -> Self: - return self - - def __exit__( - self, - exc_type: Optional[Type[BaseException]], - exc: Optional[BaseException], - exc_tb: Optional[TracebackType], - ) -> None: - self.close() - - def request( - self, - path: str, - method: Optional[str] = REQUEST_METHOD_GET, - params: ParamsType = None, - json: JsonType = None, - headers: HeadersType = None, - exclude_default_auth_headers: bool = False, - exclude_none: bool = True, - ) -> ResponseJson: - """Executes a request against the WorkOS API. - - Args: - path (str): Path for the api request that'd be appended to the base API URL - - Kwargs: - method (str): One of the supported methods as defined by the REQUEST_METHOD_X constants - params (ParamsType): Query params to be added to the request - json (JsonType): Body payload to be added to the request - exclude_none (bool): If True, removes None values from the JSON body - - Returns: - ResponseJson: Response from WorkOS - """ - prepared_request_parameters = self._prepare_request( - path=path, - method=method, - params=params, - json=json, - headers=headers, - exclude_default_auth_headers=exclude_default_auth_headers, - exclude_none=exclude_none, - ) - response = self._client.request(**prepared_request_parameters) - return self._handle_response(response) - - def delete_with_body( - self, - path: str, - json: JsonType = None, - params: ParamsType = None, - headers: HeadersType = None, - exclude_default_auth_headers: bool = False, - ) -> ResponseJson: - """Executes a DELETE request with a JSON body against the WorkOS API.""" - prepared_request_parameters = self._prepare_request( - path=path, - method=REQUEST_METHOD_DELETE, - json=json, - params=params, - headers=headers, - exclude_default_auth_headers=exclude_default_auth_headers, - force_include_body=True, - ) - response = self._client.request(**prepared_request_parameters) - return self._handle_response(response) - - -class AsyncHttpxClientWrapper(httpx.AsyncClient): - def __del__(self) -> None: - try: - asyncio.get_running_loop().create_task(self.aclose()) - except Exception: - pass - - -class AsyncHTTPClient(BaseHTTPClient[httpx.AsyncClient]): - """Async HTTP Client for a convenient way to access the WorkOS feature set.""" - - _client: httpx.AsyncClient - - _api_key: str - _client_id: str - - def __init__( - self, - *, - base_url: str, - api_key: str, - client_id: str, - version: str, - timeout: Optional[int] = None, - # If no custom transport is provided, let httpx use the default - # so we don't overwrite environment configurations like proxies - transport: Optional[httpx.AsyncBaseTransport] = None, - ) -> None: - super().__init__( - base_url=base_url, - api_key=api_key, - client_id=client_id, - version=version, - timeout=timeout, - ) - self._client = AsyncHttpxClientWrapper( - base_url=base_url, - timeout=timeout, - follow_redirects=True, - transport=transport, - ) - - def is_closed(self) -> bool: - return self._client.is_closed - - async def close(self) -> None: - """Close the underlying HTTPX client. - - The client will *not* be usable after this. - """ - await self._client.aclose() - - async def __aenter__(self) -> Self: - return self - - async def __aexit__( - self, - exc_type: Optional[Type[BaseException]], - exc: Optional[BaseException], - exc_tb: Optional[TracebackType], - ) -> None: - await self.close() - - async def request( - self, - path: str, - method: Optional[str] = REQUEST_METHOD_GET, - params: ParamsType = None, - json: JsonType = None, - headers: HeadersType = None, - exclude_default_auth_headers: bool = False, - exclude_none: bool = True, - ) -> ResponseJson: - """Executes a request against the WorkOS API. - - Args: - path (str): Path for the api request that'd be appended to the base API URL - - Kwargs: - method (str): One of the supported methods as defined by the REQUEST_METHOD_X constants - params (ParamsType): Query params to be added to the request - json (JsonType): Body payload to be added to the request - exclude_none (bool): If True, removes None values from the JSON body - - Returns: - ResponseJson: Response from WorkOS - """ - prepared_request_parameters = self._prepare_request( - path=path, - method=method, - params=params, - json=json, - headers=headers, - exclude_default_auth_headers=exclude_default_auth_headers, - exclude_none=exclude_none, - ) - response = await self._client.request(**prepared_request_parameters) - return self._handle_response(response) - - async def delete_with_body( - self, - path: str, - json: JsonType = None, - params: ParamsType = None, - headers: HeadersType = None, - exclude_default_auth_headers: bool = False, - ) -> ResponseJson: - """Executes a DELETE request with a JSON body against the WorkOS API.""" - prepared_request_parameters = self._prepare_request( - path=path, - method=REQUEST_METHOD_DELETE, - json=json, - params=params, - headers=headers, - exclude_default_auth_headers=exclude_default_auth_headers, - force_include_body=True, - ) - response = await self._client.request(**prepared_request_parameters) - return self._handle_response(response) - - -HTTPClient = Union[AsyncHTTPClient, SyncHTTPClient] diff --git a/src/workos/utils/pagination_order.py b/src/workos/utils/pagination_order.py deleted file mode 100644 index ad66990a..00000000 --- a/src/workos/utils/pagination_order.py +++ /dev/null @@ -1,4 +0,0 @@ -from typing import Literal - - -PaginationOrder = Literal["asc", "desc"] diff --git a/src/workos/utils/request_helper.py b/src/workos/utils/request_helper.py deleted file mode 100644 index 8b240255..00000000 --- a/src/workos/utils/request_helper.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import Dict, Union -import urllib.parse - - -DEFAULT_LIST_RESPONSE_LIMIT = 10 -RESPONSE_TYPE_CODE = "code" -REQUEST_METHOD_DELETE = "delete" -REQUEST_METHOD_GET = "get" -REQUEST_METHOD_PATCH = "patch" -REQUEST_METHOD_POST = "post" -REQUEST_METHOD_PUT = "put" - -QueryParameterValue = Union[str, int, bool, None] -QueryParameters = Dict[str, QueryParameterValue] - - -class RequestHelper: - @classmethod - def build_parameterized_url(cls, url: str, **params: QueryParameterValue) -> str: - escaped_params = {k: urllib.parse.quote(str(v)) for k, v in params.items()} - return url.format(**escaped_params) - - @classmethod - def build_url_with_query_params( - cls, base_url: str, path: str, **params: QueryParameterValue - ) -> str: - return base_url + path + "?" + urllib.parse.urlencode(params) diff --git a/src/workos/vault.py b/src/workos/vault.py deleted file mode 100644 index ef7b5c61..00000000 --- a/src/workos/vault.py +++ /dev/null @@ -1,543 +0,0 @@ -import base64 -from typing import Optional, Protocol, Sequence, Tuple -from workos.types.vault import VaultObject, ObjectVersion, ObjectDigest, ObjectMetadata -from workos.types.vault.key import DataKey, DataKeyPair, KeyContext, DecodedKeys -from workos.types.list_resource import ( - ListArgs, - ListMetadata, - ListPage, - WorkOSListResource, -) -from workos.utils.http_client import SyncHTTPClient -from workos.utils.request_helper import ( - DEFAULT_LIST_RESPONSE_LIMIT, - REQUEST_METHOD_DELETE, - REQUEST_METHOD_GET, - REQUEST_METHOD_POST, - REQUEST_METHOD_PUT, - RequestHelper, -) -from workos.utils.crypto_provider import CryptoProvider - -DEFAULT_RESPONSE_LIMIT = DEFAULT_LIST_RESPONSE_LIMIT - -VaultObjectList = WorkOSListResource[ObjectDigest, ListArgs, ListMetadata] - - -class VaultModule(Protocol): - def read_object(self, *, object_id: str) -> VaultObject: - """ - Get a Vault object with the value decrypted. - - Kwargs: - object_id (str): The unique identifier for the object. - Returns: - VaultObject: A vault object with metadata, name and decrypted value. - """ - ... - - def read_object_by_name(self, *, name: str) -> VaultObject: - """ - Get a Vault object by name with the value decrypted. - - Kwargs: - name (str): The unique name of the object. - Returns: - VaultObject: A vault object with metadata, name and decrypted value. - """ - ... - - def list_objects( - self, - *, - limit: int = DEFAULT_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - ) -> VaultObjectList: - """ - Gets a list of encrypted Vault objects. - - Kwargs: - limit (int): The maximum number of objects to return. (Optional) - before (str): A cursor to return resources before. (Optional) - after (str): A cursor to return resources after. (Optional) - - Returns: - VaultObjectList: A list of vault objects with built-in pagination iterator. - """ - ... - - def list_object_versions( - self, - *, - object_id: str, - ) -> Sequence[ObjectVersion]: - """ - Gets a list of versions for a specific Vault object. - - Kwargs: - object_id (str): The unique identifier for the object. - - Returns: - Sequence[ObjectVersion]: A list of object versions. - """ - ... - - def create_object( - self, - *, - name: str, - value: str, - key_context: KeyContext, - ) -> ObjectMetadata: - """ - Create a new Vault encrypted object. - - Kwargs: - name (str): The name of the object. - value (str): The value to encrypt and store. - key_context (KeyContext): A set of key-value dictionary pairs that determines which root keys to use when encrypting data. - - Returns: - VaultObject: The created vault object. - """ - ... - - def update_object( - self, - *, - object_id: str, - value: str, - version_check: Optional[str] = None, - ) -> VaultObject: - """ - Update an existing Vault object. - - Kwargs: - object_id (str): The unique identifier for the object. - value (str): The new value to encrypt and store. - version_check (str): A version of the object to prevent clobbering of data during concurrent updates. (Optional) - - Returns: - VaultObject: The updated vault object. - """ - ... - - def delete_object( - self, - *, - object_id: str, - ) -> None: - """ - Permanently delete a Vault encrypted object. Warning: this cannont be undone. - - Kwargs: - object_id (str): The unique identifier for the object. - """ - ... - - def create_data_key(self, *, key_context: KeyContext) -> DataKeyPair: - """ - Generate a data key for local encryption based on the provided key context. - The encrypted data key MUST be stored by the application, as it cannot be retrieved after generation. - - Kwargs: - key_context (KeyContext): A set of key-value dictionary pairs that determines which root keys to use when encrypting data. - """ - ... - - def decrypt_data_key( - self, - *, - keys: str, - ) -> DataKey: - """ - Decrypt encrypted data keys that were previously generated by create_data_key. - - This method takes the encrypted data key blob and uses the WorkOS Vault service - to decrypt it, returning the plaintext data key that can be used for local - encryption/decryption operations. - - Kwargs: - keys (str): The base64-encoded encrypted data key blob returned by create_data_key. - - Returns: - DataKey: The decrypted data key containing the key ID and the plaintext key material. - """ - ... - - def encrypt( - self, - *, - data: str, - key_context: KeyContext, - associated_data: Optional[str] = None, - ) -> str: - """ - Encrypt data locally using AES-GCM with a data key derived from the provided context. - - This method generates a new data key for each encryption operation, ensuring that - the same plaintext will produce different ciphertext each time it's encrypted. - The encrypted data key is embedded in the result so it can be decrypted later. - - Kwargs: - data (str): The plaintext data to encrypt. - key_context (KeyContext): A set of key-value dictionary pairs that determines which root keys to use when encrypting data. - associated_data (str): Additional authenticated data (AAD) that will be authenticated but not encrypted. (Optional) - - Returns: - str: Base64-encoded encrypted data containing the IV, authentication tag, encrypted data key, and ciphertext. - """ - ... - - def decrypt( - self, *, encrypted_data: str, associated_data: Optional[str] = None - ) -> str: - """ - Decrypt data that was previously encrypted using the encrypt method. - - This method extracts the encrypted data key from the encrypted payload, - decrypts it using the WorkOS Vault service, and then uses the resulting - data key to decrypt the actual data using AES-GCM. - - Kwargs: - encrypted_data (str): The base64-encoded encrypted data returned by the encrypt method. - associated_data (str): The same additional authenticated data (AAD) that was used during encryption, if any. (Optional) - - Returns: - str: The original plaintext data. - - Raises: - ValueError: If the encrypted_data format is invalid or if associated_data doesn't match what was used during encryption. - cryptography.exceptions.InvalidTag: If the authentication tag verification fails (data has been tampered with). - """ - ... - - -class Vault(VaultModule): - _http_client: SyncHTTPClient - _crypto_provider: CryptoProvider - - def __init__(self, http_client: SyncHTTPClient): - self._http_client = http_client - self._crypto_provider = CryptoProvider() - - def read_object( - self, - *, - object_id: str, - ) -> VaultObject: - if not object_id: - raise ValueError("Incomplete arguments: 'object_id' is a required argument") - - response = self._http_client.request( - RequestHelper.build_parameterized_url( - "vault/v1/kv/{object_id}", - object_id=object_id, - ), - method=REQUEST_METHOD_GET, - ) - - return VaultObject.model_validate(response) - - def read_object_by_name( - self, - *, - name: str, - ) -> VaultObject: - if not name: - raise ValueError("Incomplete arguments: 'name' is a required argument") - - response = self._http_client.request( - RequestHelper.build_parameterized_url( - "vault/v1/kv/name/{name}", - name=name, - ), - method=REQUEST_METHOD_GET, - ) - - return VaultObject.model_validate(response) - - def list_objects( - self, - *, - limit: int = DEFAULT_RESPONSE_LIMIT, - before: Optional[str] = None, - after: Optional[str] = None, - ) -> VaultObjectList: - list_params: ListArgs = { - "limit": limit, - "before": before, - "after": after, - } - - response = self._http_client.request( - "vault/v1/kv", - method=REQUEST_METHOD_GET, - params=list_params, - ) - - # Ensure object field is present - response_dict = dict(response) - if "object" not in response_dict: - response_dict["object"] = "list" - - return VaultObjectList( - list_method=self.list_objects, - list_args=list_params, - **ListPage[ObjectDigest](**response_dict).model_dump(), - ) - - def list_object_versions( - self, - *, - object_id: str, - ) -> Sequence[ObjectVersion]: - response = self._http_client.request( - RequestHelper.build_parameterized_url( - "vault/v1/kv/{object_id}/versions", - object_id=object_id, - ), - method=REQUEST_METHOD_GET, - ) - - return [ - ObjectVersion.model_validate(version) - for version in response.get("data", []) - ] - - def create_object( - self, - *, - name: str, - value: str, - key_context: KeyContext, - ) -> ObjectMetadata: - if not name or not value: - raise ValueError( - "Incomplete arguments: 'name' and 'value' are required arguments" - ) - - request_data = { - "name": name, - "value": value, - "key_context": key_context, - } - - response = self._http_client.request( - "vault/v1/kv", - method=REQUEST_METHOD_POST, - json=request_data, - ) - - return ObjectMetadata.model_validate(response) - - def update_object( - self, - *, - object_id: str, - value: str, - version_check: Optional[str] = None, - ) -> VaultObject: - if not object_id: - raise ValueError("Incomplete arguments: 'object_id' is a required argument") - - request_data = { - "value": value, - } - if version_check is not None: - request_data["version_check"] = version_check - - response = self._http_client.request( - RequestHelper.build_parameterized_url( - "vault/v1/kv/{object_id}", - object_id=object_id, - ), - method=REQUEST_METHOD_PUT, - json=request_data, - ) - - return VaultObject.model_validate(response) - - def delete_object( - self, - *, - object_id: str, - ) -> None: - if not object_id: - raise ValueError("Incomplete arguments: 'object_id' is a required argument") - - self._http_client.request( - RequestHelper.build_parameterized_url( - "vault/v1/kv/{object_id}", - object_id=object_id, - ), - method=REQUEST_METHOD_DELETE, - ) - - def create_data_key(self, *, key_context: KeyContext) -> DataKeyPair: - request_data = { - "context": key_context, - } - - response = self._http_client.request( - "vault/v1/keys/data-key", - method=REQUEST_METHOD_POST, - json=request_data, - ) - - return DataKeyPair.model_validate( - { - "context": response["context"], - "data_key": {"id": response["id"], "key": response["data_key"]}, - "encrypted_keys": response["encrypted_keys"], - } - ) - - def decrypt_data_key( - self, - *, - keys: str, - ) -> DataKey: - request_data = { - "keys": keys, - } - - response = self._http_client.request( - "vault/v1/keys/decrypt", - method=REQUEST_METHOD_POST, - json=request_data, - ) - - return DataKey.model_validate( - {"id": response["id"], "key": response["data_key"]} - ) - - def encrypt( - self, - *, - data: str, - key_context: KeyContext, - associated_data: Optional[str] = None, - ) -> str: - key_pair = self.create_data_key(key_context=key_context) - - key = self._base64_to_bytes(key_pair.data_key.key) - key_blob = self._base64_to_bytes(key_pair.encrypted_keys) - prefix_len_buffer = self._encode_u32(len(key_blob)) - aad_buffer = associated_data.encode("utf-8") if associated_data else None - iv = self._crypto_provider.random_bytes(12) - - result = self._crypto_provider.encrypt( - data.encode("utf-8"), key, iv, aad_buffer - ) - - combined = ( - result["iv"] - + result["tag"] - + prefix_len_buffer - + key_blob - + result["ciphertext"] - ) - - return self._bytes_to_base64(combined) - - def decrypt( - self, *, encrypted_data: str, associated_data: Optional[str] = None - ) -> str: - decoded = self._decode(encrypted_data) - data_key = self.decrypt_data_key(keys=decoded.keys) - - key = self._base64_to_bytes(data_key.key) - aad_buffer = associated_data.encode("utf-8") if associated_data else None - - decrypted_bytes = self._crypto_provider.decrypt( - ciphertext=decoded.ciphertext, - key=key, - iv=decoded.iv, - tag=decoded.tag, - aad=aad_buffer, - ) - - return decrypted_bytes.decode("utf-8") - - def _base64_to_bytes(self, data: str) -> bytes: - return base64.b64decode(data) - - def _bytes_to_base64(self, data: bytes) -> str: - return base64.b64encode(data).decode("utf-8") - - def _encode_u32(self, value: int) -> bytes: - """ - Encode a 32-bit unsigned integer as LEB128. - - Returns: - bytes: LEB128-encoded representation of the input value. - """ - if value < 0 or value > 0xFFFFFFFF: - raise ValueError("Value must be a 32-bit unsigned integer") - - encoded = bytearray() - while True: - byte = value & 0x7F - value >>= 7 - if value != 0: - byte |= 0x80 # Set continuation bit - encoded.append(byte) - if value == 0: - break - - return bytes(encoded) - - def _decode(self, encrypted_data_b64: str) -> DecodedKeys: - """ - This function extracts IV, tag, keyBlobLength, keyBlob, and ciphertext - from a base64-encoded payload. - Encoding format: [IV][TAG][4B Length][keyBlob][ciphertext] - """ - try: - payload = base64.b64decode(encrypted_data_b64) - except Exception as e: - raise ValueError("Base64 decoding failed") from e - - iv = payload[0:12] - tag = payload[12:28] - - try: - key_len, leb_len = self._decode_u32(payload[28:]) - except Exception as e: - raise ValueError("Failed to decode key length") from e - - keys_index = 28 + leb_len - keys_end = keys_index + key_len - keys_slice = payload[keys_index:keys_end] - keys = base64.b64encode(keys_slice).decode("utf-8") - ciphertext = payload[keys_end:] - - return DecodedKeys(iv=iv, tag=tag, keys=keys, ciphertext=ciphertext) - - def _decode_u32(self, buf: bytes) -> Tuple[int, int]: - """ - Decode an unsigned LEB128-encoded 32-bit integer from bytes. - - Returns: - (value, length_consumed) - - Raises: - ValueError if decoding fails or overflows. - """ - res = 0 - bit = 0 - - for i, b in enumerate(buf): - if i > 4: - raise ValueError("LEB128 integer overflow (was more than 4 bytes)") - - res |= (b & 0x7F) << (7 * bit) - - if (b & 0x80) == 0: - return res, i + 1 - - bit += 1 - - raise ValueError("LEB128 integer not found") diff --git a/src/workos/webhooks.py b/src/workos/webhooks.py deleted file mode 100644 index 948210db..00000000 --- a/src/workos/webhooks.py +++ /dev/null @@ -1,142 +0,0 @@ -import hashlib -import hmac -import time -from typing import Optional, Protocol -from workos.types.webhooks.webhook import Webhook -from workos.types.webhooks.webhook_payload import WebhookPayload -from workos.typing.webhooks import WebhookTypeAdapter - - -class WebhooksModule(Protocol): - """Offers methods through the WorkOS Webhooks service.""" - - def verify_event( - self, - *, - event_body: WebhookPayload, - event_signature: str, - secret: str, - tolerance: Optional[int] = None, - ) -> Webhook: - """Verify and deserialize the signature of a Webhook event. - - Kwargs: - event_body (WebhookPayload): The Webhook body. - event_signature (str): The signature of the Webhook from the 'WorkOS-Signature' header. - secret (str): The secret for the webhook endpoint, you can find this in the WorkOS dashboard. - tolerance (int): The number of seconds the Webhook event is valid for. (Optional) - Returns: - Webhook: The deserialized Webhook. - """ - ... - - def verify_header( - self, - *, - event_body: WebhookPayload, - event_signature: str, - secret: str, - tolerance: Optional[int] = None, - ) -> None: - """Verify the signature of a Webhook, raise ValueError if the signature can't be verified. - - Kwargs: - event_body (WebhookPayload): The Webhook body. - event_signature (str): The signature of the Webhook from the 'WorkOS-Signature' header. - secret (str): The secret for the webhook endpoint, you can find this in the WorkOS dashboard. - tolerance (int): The number of seconds the Webhook event is valid for. (Optional) - Returns: - None - """ - ... - - def _constant_time_compare(self, val1: str, val2: str) -> bool: ... - - def _check_timestamp_range(self, time: float, max_range: float) -> None: ... - - -class Webhooks(WebhooksModule): - DEFAULT_TOLERANCE = 180 - - def verify_event( - self, - *, - event_body: WebhookPayload, - event_signature: str, - secret: str, - tolerance: Optional[int] = DEFAULT_TOLERANCE, - ) -> Webhook: - Webhooks.verify_header( - self, - event_body=event_body, - event_signature=event_signature, - secret=secret, - tolerance=tolerance, - ) - return WebhookTypeAdapter.validate_json(event_body) - - def verify_header( - self, - *, - event_body: WebhookPayload, - event_signature: str, - secret: str, - tolerance: Optional[int] = None, - ) -> None: - try: - # Verify and define variables parsed from the event body - issued_timestamp, signature_hash = event_signature.split(", ") - except BaseException: - raise ValueError( - "Unable to extract timestamp and signature hash from header", - event_signature, - ) - - issued_timestamp = issued_timestamp[2:] - signature_hash = signature_hash[3:] - max_seconds_since_issued = tolerance or Webhooks.DEFAULT_TOLERANCE - current_time = time.time() - timestamp_in_seconds = int(issued_timestamp) / 1000 - seconds_since_issued = current_time - timestamp_in_seconds - - # Check that the webhook timestamp is within the acceptable range - Webhooks._check_timestamp_range( - self, seconds_since_issued, max_seconds_since_issued - ) - - # Set expected signature value based on env var secret - unhashed_string = "{0}.{1}".format(issued_timestamp, event_body.decode("utf-8")) - expected_signature = hmac.new( - secret.encode("utf-8"), - unhashed_string.encode("utf-8"), - digestmod=hashlib.sha256, - ).hexdigest() - - # Use constant time comparison function to ensure the sig hash matches - # the expected sig value - secure_compare = Webhooks._constant_time_compare( - self, signature_hash, expected_signature - ) - if not secure_compare: - raise ValueError( - "Signature hash does not match the expected signature hash for payload" - ) - - def _constant_time_compare(self, val1: str, val2: str) -> bool: - if len(val1) != len(val2): - return False - - result = 0 - for x, y in zip(val1, val2): - result |= ord(x) ^ ord(y) - if result != 0: - return False - - if result == 0: - return True - - return False - - def _check_timestamp_range(self, time: float, max_range: float) -> None: - if time > max_range: - raise ValueError("Timestamp outside the tolerance zone") diff --git a/src/workos/widgets.py b/src/workos/widgets.py deleted file mode 100644 index a6508b91..00000000 --- a/src/workos/widgets.py +++ /dev/null @@ -1,54 +0,0 @@ -from typing import Protocol, Sequence -from workos.types.widgets.widget_scope import WidgetScope -from workos.types.widgets.widget_token_response import WidgetTokenResponse -from workos.utils.http_client import SyncHTTPClient -from workos.utils.request_helper import REQUEST_METHOD_POST - - -WIDGETS_GENERATE_TOKEN_PATH = "widgets/token" - - -class WidgetsModule(Protocol): - def get_token( - self, - *, - organization_id: str, - user_id: str, - scopes: Sequence[WidgetScope], - ) -> WidgetTokenResponse: - """Generate a new widget token for the specified organization and user with the provided scopes. - - Kwargs: - organization_id (str): The ID of the organization the widget token will be generated for. - user_id (str): The ID of the AuthKit user the widget token will be generated for. - scopes (Sequence[WidgetScope]): The widget scopes for the generated widget token. - - Returns: - WidgetTokenResponse: WidgetTokenResponse object with token string. - """ - ... - - -class Widgets(WidgetsModule): - _http_client: SyncHTTPClient - - def __init__(self, http_client: SyncHTTPClient): - self._http_client = http_client - - def get_token( - self, - *, - organization_id: str, - user_id: str, - scopes: Sequence[WidgetScope], - ) -> WidgetTokenResponse: - json = { - "organization_id": organization_id, - "user_id": user_id, - "scopes": scopes, - } - response = self._http_client.request( - WIDGETS_GENERATE_TOKEN_PATH, method=REQUEST_METHOD_POST, json=json - ) - - return WidgetTokenResponse.model_validate(response) diff --git a/tests/conftest.py b/tests/conftest.py index 8b8ad8a3..a30f909b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,351 +1,18 @@ -from typing import ( - Any, - Awaitable, - Callable, - Literal, - Mapping, - Optional, - Sequence, - Tuple, - Union, - cast, -) -from unittest.mock import AsyncMock, MagicMock -import urllib.parse - -import inspect - -import httpx import pytest -from functools import wraps - -from tests.utils.client_configuration import ClientConfiguration -from tests.utils.list_resource import list_data_to_dicts, list_response_of -from tests.utils.syncify import syncify -from tests.types.test_auto_pagination_function import TestAutoPaginationFunction -from workos.types.list_resource import WorkOSListResource -from workos.utils._base_http_client import DEFAULT_REQUEST_TIMEOUT -from workos.utils.http_client import AsyncHTTPClient, HTTPClient, SyncHTTPClient -from workos.utils.request_helper import DEFAULT_LIST_RESPONSE_LIMIT - -from jwt import PyJWKClient -from unittest.mock import Mock, patch - - -def _get_test_client_setup( - http_client_class_name: str, -) -> Tuple[Literal["async", "sync"], ClientConfiguration, HTTPClient]: - base_url = "https://api.workos.test/" - client_id = "client_b27needthisforssotemxo" - - setup_name = None - if http_client_class_name == "AsyncHTTPClient": - http_client = AsyncHTTPClient( - api_key="sk_test", - base_url=base_url, - client_id=client_id, - version="test", - ) - setup_name = "async" - elif http_client_class_name == "SyncHTTPClient": - http_client = SyncHTTPClient( - api_key="sk_test", - base_url=base_url, - client_id=client_id, - version="test", - ) - setup_name = "sync" - else: - raise ValueError( - f"Invalid HTTP client for test module setup: {http_client_class_name}" - ) - - client_configuration = ClientConfiguration( - base_url=base_url, client_id=client_id, request_timeout=DEFAULT_REQUEST_TIMEOUT - ) - - return setup_name, client_configuration, http_client - - -def pytest_configure(config) -> None: - config.addinivalue_line( - "markers", - "sync_and_async(): mark test to run both sync and async module versions", - ) - - -def pytest_generate_tests(metafunc: pytest.Metafunc): - for marker in metafunc.definition.iter_markers(name="sync_and_async"): - if marker.name == "sync_and_async": - if len(marker.args) == 0: - raise ValueError( - "sync_and_async marker requires argument representing list of modules." - ) - - # Take in args as a list of module classes. For example: - # @pytest.mark.sync_and_async(Events, AsyncEvents) -> [Events, AsyncEvents] - module_classes = marker.args - ids = [] - arg_values = [] - arg_names = ["module_instance"] - - for module_class in module_classes: - if module_class is None: - raise ValueError( - f"Invalid module class for sync_and_async marker: {module_class}" - ) - - # Pull the HTTP client type from the module class annotations and use that - # to pass in the proper test HTTP client - http_client_name = module_class.__annotations__["_http_client"].__name__ - setup_name, client_configuration, http_client = _get_test_client_setup( - http_client_name - ) - - class_kwargs: Mapping[str, Any] = {"http_client": http_client} - if module_class.__init__.__annotations__.get( - "client_configuration", None - ): - class_kwargs["client_configuration"] = client_configuration - - module_instance = module_class(**class_kwargs) - - ids.append(setup_name) # sync or async will be the test ID - arg_values.append([module_instance]) - - metafunc.parametrize( - argnames=arg_names, argvalues=arg_values, ids=ids, scope="class" - ) - - -@pytest.fixture -def sync_http_client_for_test(): - _, _, http_client = _get_test_client_setup("SyncHTTPClient") - return http_client - - -@pytest.fixture -def sync_client_configuration_and_http_client_for_test(): - _, client_configuration, http_client = _get_test_client_setup("SyncHTTPClient") - return client_configuration, http_client - - -@pytest.fixture -def mock_http_client_with_response(monkeypatch): - def inner( - http_client: HTTPClient, - response_dict: Optional[dict] = None, - status_code: int = 200, - headers: Optional[Mapping[str, str]] = None, - ): - mock_class = ( - AsyncMock if isinstance(http_client, AsyncHTTPClient) else MagicMock - ) - mock = mock_class( - return_value=httpx.Response( - status_code=status_code, headers=headers, json=response_dict - ), - ) - monkeypatch.setattr(http_client._client, "request", mock) - - return inner - - -@pytest.fixture -def capture_and_mock_http_client_request(monkeypatch): - def inner( - http_client: HTTPClient, - response_dict: Optional[dict] = None, - status_code: int = 200, - headers: Optional[Mapping[str, str]] = None, - ): - request_kwargs = {} - - def capture_and_mock(*args, **kwargs): - request_kwargs.update(kwargs) - - # Capture full URL with encoded params while keeping original URL - if kwargs and "params" in kwargs and kwargs["params"]: - # Convert params to query string with proper URL encoding - query_string = urllib.parse.urlencode( - kwargs["params"], doseq=True, quote_via=urllib.parse.quote_plus - ) - request_kwargs.update({"full_url": f"{kwargs['url']}?{query_string}"}) - - return httpx.Response( - status_code=status_code, - headers=headers, - json=response_dict, - ) - - mock_class = ( - AsyncMock if isinstance(http_client, AsyncHTTPClient) else MagicMock - ) - mock = mock_class(side_effect=capture_and_mock) - - monkeypatch.setattr(http_client._client, "request", mock) - - return request_kwargs - - return inner +from workos import WorkOS, AsyncWorkOS @pytest.fixture -def capture_and_mock_pagination_request_for_http_client(monkeypatch): - # Mocking pagination correctly requires us to index into a list of data - # and correctly set the before and after metadata in the response. - def inner( - http_client: HTTPClient, - data_list: list, - status_code: int = 200, - headers: Optional[Mapping[str, str]] = None, - ): - request_kwargs = {} - - # For convenient index lookup, store the list of object IDs. - data_ids = list(map(lambda x: x["id"], data_list)) - - def mock_function(*args, **kwargs): - request_kwargs.update(kwargs) - - params = kwargs.get("params") or {} - request_after = params.get("after", None) - limit = params.get("limit", 10) - - if request_after is None: - # First page - start = 0 - else: - # A subsequent page, return the first item _after_ the index we locate - start = data_ids.index(request_after) + 1 - data = data_list[start : start + limit] - if len(data) < limit or len(data) == 0: - # No more data, set after to None - after = None - else: - # Set after to the last item in this page of results - after = data[-1]["id"] - - return httpx.Response( - status_code=status_code, - headers=headers, - json=list_response_of(data=data, before=request_after, after=after), - ) - - mock_class = ( - AsyncMock if isinstance(http_client, AsyncHTTPClient) else MagicMock - ) - mock = mock_class(side_effect=mock_function) - - monkeypatch.setattr(http_client._client, "request", mock) - - return request_kwargs - - return inner +def workos(): + return WorkOS( + api_key="sk_test", + client_id="client_test", + ) @pytest.fixture -def test_auto_pagination( - capture_and_mock_pagination_request_for_http_client, -) -> TestAutoPaginationFunction: - def _iterate_results_sync( - list_function: Callable[[], WorkOSListResource], - list_function_params: Optional[Mapping[str, Any]] = None, - ) -> Sequence[Any]: - results = list_function(**list_function_params or {}) - all_results = [] - - for result in results: - all_results.append(result) - - return all_results - - async def _iterate_results_async( - list_function: Callable[[], Awaitable[WorkOSListResource]], - list_function_params: Optional[Mapping[str, Any]] = None, - ) -> Sequence[Any]: - results = await list_function(**list_function_params or {}) - all_results = [] - - async for result in results: - all_results.append(result) - - return all_results - - def inner( - http_client: HTTPClient, - list_function: Union[ - Callable[[], WorkOSListResource], - Callable[[], Awaitable[WorkOSListResource]], - ], - expected_all_page_data: dict, - list_function_params: Optional[Mapping[str, Any]] = None, - url_path_keys: Optional[Sequence[str]] = None, - ) -> None: - request_kwargs = capture_and_mock_pagination_request_for_http_client( - http_client=http_client, - data_list=expected_all_page_data, - status_code=200, - ) - - all_results = [] - if isinstance(http_client, AsyncHTTPClient): - all_results = syncify( - _iterate_results_async( - cast(Callable[[], Awaitable[WorkOSListResource]], list_function), - list_function_params, - ) - ) - else: - all_results = _iterate_results_sync( - cast(Callable[[], WorkOSListResource], list_function), - list_function_params, - ) - - assert len(list(all_results)) == len(expected_all_page_data) - assert (list_data_to_dicts(all_results)) == expected_all_page_data - assert request_kwargs["method"] == "get" - - # Validate parameters - assert "after" in request_kwargs["params"] - assert request_kwargs["params"]["limit"] == DEFAULT_LIST_RESPONSE_LIMIT - if "order" in request_kwargs["params"]: - assert request_kwargs["params"]["order"] == "desc" - - params = list_function_params or {} - for param in params: - if url_path_keys is not None and param not in url_path_keys: - assert request_kwargs["params"][param] == params[param] - - return inner - - -def with_jwks_mock(func): - @wraps(func) - async def async_wrapper(*args, **kwargs): - # Create mock JWKS client - mock_jwks = Mock(spec=PyJWKClient) - mock_signing_key = Mock() - mock_signing_key.key = kwargs["session_constants"]["PUBLIC_KEY"] - mock_jwks.get_signing_key_from_jwt.return_value = mock_signing_key - - # Apply the mock - with patch("workos.session.PyJWKClient", return_value=mock_jwks): - return await func(*args, **kwargs) - - @wraps(func) - def sync_wrapper(*args, **kwargs): - # Create mock JWKS client - mock_jwks = Mock(spec=PyJWKClient) - mock_signing_key = Mock() - mock_signing_key.key = kwargs["session_constants"]["PUBLIC_KEY"] - mock_jwks.get_signing_key_from_jwt.return_value = mock_signing_key - - # Apply the mock - with patch("workos.session.PyJWKClient", return_value=mock_jwks): - return func(*args, **kwargs) - - # Return appropriate wrapper based on whether the function is async or not - if inspect.iscoroutinefunction(func): - return async_wrapper - return sync_wrapper +def async_workos(): + return AsyncWorkOS( + api_key="sk_test", + client_id="client_test", + ) diff --git a/tests/smoke_test.py b/tests/smoke_test.py deleted file mode 100644 index 71cbc32a..00000000 --- a/tests/smoke_test.py +++ /dev/null @@ -1,272 +0,0 @@ -#!/usr/bin/env python3 -"""Smoke tests to verify the built package works correctly. - -These tests run against the installed package (wheel or sdist) to verify: -- All imports work correctly -- Dependencies are properly bundled -- Type markers are present -- Both sync and async clients can be instantiated -- All module properties are accessible - -Run with: uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py -""" - -# ruff: noqa: F401 - imports are intentionally unused; this file tests import functionality - -import sys -from pathlib import Path - - -def test_basic_import() -> None: - """Verify the package can be imported.""" - import workos - - assert workos is not None - print("✓ Basic import works") - - -def test_version_accessible() -> None: - """Verify version metadata is accessible.""" - from importlib.metadata import version - - pkg_version = version("workos") - assert pkg_version is not None - assert len(pkg_version) > 0 - print(f"✓ Version accessible: {pkg_version}") - - -def test_py_typed_marker() -> None: - """Verify py.typed marker is included for type checking support.""" - import workos - - package_path = Path(workos.__file__).parent - py_typed = package_path / "py.typed" - assert py_typed.exists(), f"py.typed marker not found at {py_typed}" - print(f"✓ py.typed marker present at {py_typed}") - - -def test_sync_client_import_and_instantiate() -> None: - """Verify sync client can be imported and instantiated.""" - from workos import WorkOSClient - - # Instantiate with dummy credentials (no API calls made) - client = WorkOSClient(api_key="sk_test_smoke", client_id="client_smoke") - assert client is not None - print("✓ WorkOSClient imports and instantiates") - - -def test_async_client_import_and_instantiate() -> None: - """Verify async client can be imported and instantiated.""" - from workos import AsyncWorkOSClient - - # Instantiate with dummy credentials (no API calls made) - client = AsyncWorkOSClient(api_key="sk_test_smoke", client_id="client_smoke") - assert client is not None - print("✓ AsyncWorkOSClient imports and instantiates") - - -def test_sync_client_modules_accessible() -> None: - """Verify all module properties are accessible on sync client.""" - from workos import WorkOSClient - - client = WorkOSClient(api_key="sk_test_smoke", client_id="client_smoke") - - modules = [ - "api_keys", - "audit_logs", - "directory_sync", - "events", - "fga", - "mfa", - "organizations", - "organization_domains", - "passwordless", - "pipes", - "portal", - "sso", - "user_management", - "vault", - "webhooks", - "widgets", - ] - - for module_name in modules: - module = getattr(client, module_name, None) - assert module is not None, f"Module {module_name} not accessible" - print(f" ✓ client.{module_name}") - - print(f"✓ All {len(modules)} sync client modules accessible") - - -def test_async_client_modules_accessible() -> None: - """Verify all module properties are accessible on async client. - - Note: Some modules raise NotImplementedError as they're not yet - supported in the async client. We verify the property exists and - raises the expected error. - """ - from workos import AsyncWorkOSClient - - client = AsyncWorkOSClient(api_key="sk_test_smoke", client_id="client_smoke") - - # Modules fully supported in async client - supported_modules = [ - "api_keys", - "audit_logs", - "directory_sync", - "events", - "organizations", - "organization_domains", - "pipes", - "sso", - "user_management", - ] - - # Modules that exist but raise NotImplementedError - not_implemented_modules = [ - "fga", - "mfa", - "passwordless", - "portal", - "vault", - "webhooks", - "widgets", - ] - - for module_name in supported_modules: - module = getattr(client, module_name, None) - assert module is not None, f"Module {module_name} not accessible" - print(f" ✓ async_client.{module_name}") - - for module_name in not_implemented_modules: - try: - getattr(client, module_name) - raise AssertionError( - f"Module {module_name} should raise NotImplementedError" - ) - except NotImplementedError: - print(f" ✓ async_client.{module_name} (not yet implemented)") - - total = len(supported_modules) + len(not_implemented_modules) - print(f"✓ All {total} async client modules verified") - - -def test_core_types_importable() -> None: - """Verify core type models can be imported.""" - # SSO types - from workos.types.sso import Connection, ConnectionDomain, Profile - - assert Connection is not None - assert ConnectionDomain is not None - assert Profile is not None - - # Organization types - from workos.types.organizations import Organization - - assert Organization is not None - - # Directory Sync types - from workos.types.directory_sync import Directory, DirectoryGroup, DirectoryUser - - assert Directory is not None - assert DirectoryGroup is not None - assert DirectoryUser is not None - - # User Management types - from workos.types.user_management import ( - AuthenticationResponse, - Invitation, - OrganizationMembership, - User, - ) - - assert AuthenticationResponse is not None - assert Invitation is not None - assert OrganizationMembership is not None - assert User is not None - - # Events types - from workos.types.events import Event - - assert Event is not None - - # FGA types - from workos.types.fga import Warrant, CheckResponse - - assert Warrant is not None - assert CheckResponse is not None - - print("✓ Core types importable") - - -def test_exceptions_importable() -> None: - """Verify exception classes can be imported.""" - from workos.exceptions import ( - AuthenticationException, - AuthorizationException, - BadRequestException, - ConflictException, - NotFoundException, - ServerException, - ) - - assert AuthenticationException is not None - assert AuthorizationException is not None - assert BadRequestException is not None - assert ConflictException is not None - assert NotFoundException is not None - assert ServerException is not None - - print("✓ Exception classes importable") - - -def test_dependencies_available() -> None: - """Verify core dependencies are installed and importable.""" - import httpx - import pydantic - import cryptography - import jwt - - print("✓ Core dependencies available (httpx, pydantic, cryptography, jwt)") - - -def main() -> int: - """Run all smoke tests.""" - print("=" * 60) - print("WorkOS Python SDK - Smoke Tests") - print("=" * 60) - print() - - tests = [ - test_basic_import, - test_version_accessible, - test_py_typed_marker, - test_sync_client_import_and_instantiate, - test_async_client_import_and_instantiate, - test_sync_client_modules_accessible, - test_async_client_modules_accessible, - test_core_types_importable, - test_exceptions_importable, - test_dependencies_available, - ] - - failed = 0 - for test in tests: - try: - test() - except Exception as e: - print(f"✗ {test.__name__} FAILED: {e}") - failed += 1 - print() - - print("=" * 60) - if failed == 0: - print(f"All {len(tests)} smoke tests passed!") - return 0 - else: - print(f"FAILED: {failed}/{len(tests)} tests failed") - return 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tests/test_api_keys.py b/tests/test_api_keys.py deleted file mode 100644 index e86dcdf0..00000000 --- a/tests/test_api_keys.py +++ /dev/null @@ -1,70 +0,0 @@ -# type: ignore -import pytest - -from tests.utils.fixtures.mock_api_key import MockApiKey -from tests.utils.syncify import syncify -from workos.api_keys import ( - API_KEYS_PATH, - API_KEY_VALIDATION_PATH, - ApiKeys, - AsyncApiKeys, -) - - -@pytest.mark.sync_and_async(ApiKeys, AsyncApiKeys) -class TestApiKeys: - @pytest.fixture - def mock_api_key(self): - return MockApiKey().dict() - - @pytest.fixture - def api_key(self): - return "sk_my_api_key" - - def test_validate_api_key_with_valid_key( - self, - module_instance, - api_key, - mock_api_key, - capture_and_mock_http_client_request, - ): - response_body = {"api_key": mock_api_key} - request_kwargs = capture_and_mock_http_client_request( - module_instance._http_client, response_body, 200 - ) - - api_key_details = syncify(module_instance.validate_api_key(value=api_key)) - - assert request_kwargs["url"].endswith(API_KEY_VALIDATION_PATH) - assert request_kwargs["method"] == "post" - assert api_key_details.id == mock_api_key["id"] - assert api_key_details.name == mock_api_key["name"] - assert api_key_details.object == "api_key" - - def test_validate_api_key_with_invalid_key( - self, - module_instance, - mock_http_client_with_response, - ): - mock_http_client_with_response( - module_instance._http_client, - {"api_key": None}, - 200, - ) - - assert syncify(module_instance.validate_api_key(value="invalid-key")) is None - - def test_delete_api_key( - self, - module_instance, - capture_and_mock_http_client_request, - ): - api_key_id = "api_key_01234567890" - request_kwargs = capture_and_mock_http_client_request( - module_instance._http_client, {}, 204 - ) - - syncify(module_instance.delete_api_key(api_key_id)) - - assert request_kwargs["url"].endswith(f"{API_KEYS_PATH}/{api_key_id}") - assert request_kwargs["method"] == "delete" diff --git a/tests/test_async_http_client.py b/tests/test_async_http_client.py deleted file mode 100644 index b842c51f..00000000 --- a/tests/test_async_http_client.py +++ /dev/null @@ -1,368 +0,0 @@ -from platform import python_version - -import httpx -import pytest -from unittest.mock import AsyncMock - -from tests.test_sync_http_client import STATUS_CODE_TO_EXCEPTION_MAPPING -from workos.exceptions import ( - BadRequestException, - BaseRequestException, - ConflictException, - ServerException, -) -from workos.utils.http_client import AsyncHTTPClient - - -@pytest.mark.asyncio -class TestAsyncHTTPClient(object): - @pytest.fixture(autouse=True) - def setup(self): - response = httpx.Response(200, json={"message": "Success!"}) - - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"message": "Success!"}) - - self.http_client = AsyncHTTPClient( - api_key="sk_test", - base_url="https://api.workos.test/", - client_id="client_b27needthisforssotemxo", - version="test", - transport=httpx.MockTransport(handler), - ) - - self.http_client._client.request = AsyncMock( - return_value=response, - ) - - @pytest.mark.parametrize( - "method,status_code,expected_response", - [ - ("GET", 200, {"message": "Success!"}), - ("DELETE", 204, None), - ("DELETE", 202, None), - ], - ) - async def test_request_without_body( - self, method: str, status_code: int, expected_response: dict - ): - self.http_client._client.request = AsyncMock( - return_value=httpx.Response( - status_code=status_code, json=expected_response - ), - ) - - response = await self.http_client.request( - "events", method=method, params={"test_param": "test_value"} - ) - - self.http_client._client.request.assert_called_with( - method=method, - url="https://api.workos.test/events", - headers=httpx.Headers( - { - "accept": "application/json", - "content-type": "application/json", - "user-agent": f"WorkOS Python/{python_version()} Python SDK/test", - "authorization": "Bearer sk_test", - } - ), - params={"test_param": "test_value"}, - timeout=25, - ) - - assert response == expected_response - - @pytest.mark.parametrize( - "method,status_code,expected_response", - [ - ("POST", 201, {"message": "Success!"}), - ("PUT", 200, {"message": "Success!"}), - ("PATCH", 200, {"message": "Success!"}), - ], - ) - async def test_request_with_body( - self, method: str, status_code: int, expected_response: dict - ): - self.http_client._client.request = AsyncMock( - return_value=httpx.Response( - status_code=status_code, json=expected_response - ), - ) - - response = await self.http_client.request( - "events", method=method, json={"test_param": "test_value"} - ) - - self.http_client._client.request.assert_called_with( - method=method, - url="https://api.workos.test/events", - headers=httpx.Headers( - { - "accept": "application/json", - "content-type": "application/json", - "user-agent": f"WorkOS Python/{python_version()} Python SDK/test", - "authorization": "Bearer sk_test", - } - ), - params=None, - json={"test_param": "test_value"}, - timeout=25, - ) - - assert response == expected_response - - @pytest.mark.parametrize( - "method,status_code,expected_response", - [ - ("POST", 201, {"message": "Success!"}), - ("PUT", 200, {"message": "Success!"}), - ("PATCH", 200, {"message": "Success!"}), - ], - ) - async def test_request_with_body_and_query_parameters( - self, method: str, status_code: int, expected_response: dict - ): - self.http_client._client.request = AsyncMock( - return_value=httpx.Response( - status_code=status_code, json=expected_response - ), - ) - - response = await self.http_client.request( - "events", - method=method, - params={"test_param": "test_param_value"}, - json={"test_json": "test_json_value"}, - ) - - self.http_client._client.request.assert_called_with( - method=method, - url="https://api.workos.test/events", - headers=httpx.Headers( - { - "accept": "application/json", - "content-type": "application/json", - "user-agent": f"WorkOS Python/{python_version()} Python SDK/test", - "authorization": "Bearer sk_test", - } - ), - params={"test_param": "test_param_value"}, - json={"test_json": "test_json_value"}, - timeout=25, - ) - - assert response == expected_response - - @pytest.mark.parametrize( - "status_code,expected_exception", - STATUS_CODE_TO_EXCEPTION_MAPPING, - ) - async def test_request_raises_expected_exception_for_status_code( - self, status_code: int, expected_exception: BaseRequestException - ): - self.http_client._client.request = AsyncMock( - return_value=httpx.Response(status_code=status_code), - ) - - with pytest.raises(expected_exception): # type: ignore - await self.http_client.request("bad_place") - - @pytest.mark.parametrize( - "status_code,expected_exception", - STATUS_CODE_TO_EXCEPTION_MAPPING, - ) - async def test_request_exceptions_include_expected_request_data( - self, status_code: int, expected_exception: BaseRequestException - ): - request_id = "request-123" - response_message = "stuff happened" - - self.http_client._client.request = AsyncMock( - return_value=httpx.Response( - status_code=status_code, - json={"message": response_message}, - headers={"X-Request-ID": request_id}, - ), - ) - - try: - await self.http_client.request("bad_place") - except expected_exception as ex: # type: ignore - assert ex.message == response_message - assert ex.request_id == request_id - assert ex.__class__ == expected_exception - - async def test_bad_request_exceptions_include_expected_request_data(self): - request_id = "request-123" - error = "example_error" - error_description = "Example error description" - - self.http_client._client.request = AsyncMock( - return_value=httpx.Response( - status_code=400, - json={"error": error, "error_description": error_description}, - headers={"X-Request-ID": request_id}, - ), - ) - - try: - await self.http_client.request("bad_place") - except BadRequestException as ex: - assert ( - str(ex) - == "(message=No message, request_id=request-123, error=example_error, error_description=Example error description)" - ) - assert ex.__class__ == BadRequestException - - async def test_bad_request_exceptions_exclude_expected_request_data(self): - request_id = "request-123" - - self.http_client._client.request = AsyncMock( - return_value=httpx.Response( - status_code=400, - json={"foo": "bar"}, - headers={"X-Request-ID": request_id}, - ), - ) - - try: - await self.http_client.request("bad_place") - except BadRequestException as ex: - assert str(ex) == "(message=No message, request_id=request-123, foo=bar)" - assert ex.__class__ == BadRequestException - - async def test_request_bad_body_raises_expected_exception_with_request_data(self): - request_id = "request-123" - - self.http_client._client.request = AsyncMock( - return_value=httpx.Response( - status_code=200, - content="this_isnt_json", - headers={"X-Request-ID": request_id}, - ), - ) - - try: - await self.http_client.request("bad_place") - except ServerException as ex: - assert ex.message is None - assert ex.request_id == request_id - assert ex.__class__ == ServerException - - async def test_conflict_exception(self): - request_id = "request-123" - - self.http_client._client.request = AsyncMock( - return_value=httpx.Response( - status_code=409, - headers={"X-Request-ID": request_id}, - ), - ) - - try: - await self.http_client.request("bad_place") - except ConflictException as ex: - assert str(ex) == "(message=No message, request_id=request-123)" - assert ex.__class__ == ConflictException - - async def test_request_includes_base_headers( - self, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 200) - - await self.http_client.request("ok_place") - - default_headers = set( - (header[0].lower(), header[1]) - for header in self.http_client.default_headers.items() - ) - headers = set(request_kwargs["headers"].items()) - - assert default_headers.issubset(headers) - - async def test_request_parses_json_when_content_type_present(self): - self.http_client._client.request = AsyncMock( - return_value=httpx.Response( - status_code=200, - json={"foo": "bar"}, - headers={"content-type": "application/json"}, - ), - ) - - assert await self.http_client.request("ok_place") == {"foo": "bar"} - - async def test_request_parses_json_when_encoding_in_content_type(self): - self.http_client._client.request = AsyncMock( - return_value=httpx.Response( - status_code=200, - json={"foo": "bar"}, - headers={"content-type": "application/json; charset=utf8"}, - ), - ) - - assert await self.http_client.request("ok_place") == {"foo": "bar"} - - async def test_request_removes_none_parameter_values( - self, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 200) - - await self.http_client.request( - path="/test", - method="get", - params={"organization_id": None, "test": "value"}, - ) - - assert request_kwargs["params"] == {"test": "value"} - - async def test_request_removes_none_json_values( - self, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 200) - - await self.http_client.request( - path="/test", - method="post", - json={"organization_id": None, "test": "value"}, - ) - - assert request_kwargs["url"].endswith("/test") - assert request_kwargs["json"] == {"test": "value"} - - async def test_delete_with_body_sends_json( - self, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 200) - - await self.http_client.delete_with_body( - path="/test", - json={"obj": "json"}, - ) - - assert request_kwargs["method"] == "delete" - assert request_kwargs["json"] == {"obj": "json"} - - async def test_delete_with_body_sends_params( - self, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 200) - - await self.http_client.delete_with_body( - path="/test", - json={"obj1": "json"}, - params={"obj2": "params"}, - ) - - assert request_kwargs["json"] == {"obj1": "json"} - assert request_kwargs["params"] == {"obj2": "params"} - - async def test_delete_without_body_raises_value_error(self): - with pytest.raises( - ValueError, match="Cannot send a body with a delete request" - ): - await self.http_client.request( - path="/test", - method="delete", - json={"should": "fail"}, - ) diff --git a/tests/test_audit_logs.py b/tests/test_audit_logs.py deleted file mode 100644 index 4bde30a9..00000000 --- a/tests/test_audit_logs.py +++ /dev/null @@ -1,883 +0,0 @@ -from datetime import datetime -from typing import Union - -import pytest - -from tests.utils.syncify import syncify -from workos.audit_logs import AuditLogEvent, AuditLogs, AsyncAuditLogs -from workos.exceptions import AuthenticationException, BadRequestException - - -@pytest.mark.sync_and_async(AuditLogs, AsyncAuditLogs) -class TestAuditLogs: - @pytest.fixture - def mock_audit_log_event(self) -> AuditLogEvent: - return { - "action": "document.updated", - "occurred_at": datetime.now().isoformat(), - "actor": { - "id": "user_1", - "name": "Jon Smith", - "type": "user", - }, - "targets": [ - { - "id": "document_39127", - "type": "document", - }, - ], - "context": { - "location": "192.0.0.8", - "user_agent": "Firefox", - }, - "metadata": { - "successful": True, - }, - } - - class TestCreateEvent: - def test_succeeds( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - capture_and_mock_http_client_request, - ): - organization_id = "org_123456789" - - event: AuditLogEvent = { - "action": "document.updated", - "occurred_at": datetime.now().isoformat(), - "actor": { - "id": "user_1", - "name": "Jon Smith", - "type": "user", - }, - "targets": [ - { - "id": "document_39127", - "type": "document", - }, - ], - "context": { - "location": "192.0.0.8", - "user_agent": "Firefox", - }, - "metadata": { - "successful": True, - }, - } - - request_kwargs = capture_and_mock_http_client_request( - http_client=module_instance._http_client, - response_dict={"success": True}, - status_code=200, - ) - - response = syncify( - module_instance.create_event( - organization_id=organization_id, - event=event, - idempotency_key="test_123456", - ) - ) - - assert request_kwargs["url"].endswith("/audit_logs/events") - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == { - "organization_id": organization_id, - "event": event, - } - assert response is None - - def test_sends_idempotency_key( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - mock_audit_log_event, - capture_and_mock_http_client_request, - ): - idempotency_key = "test_123456789" - - organization_id = "org_123456789" - - request_kwargs = capture_and_mock_http_client_request( - module_instance._http_client, {"success": True}, 200 - ) - - response = syncify( - module_instance.create_event( - organization_id=organization_id, - event=mock_audit_log_event, - idempotency_key=idempotency_key, - ) - ) - - assert request_kwargs["headers"]["idempotency-key"] == idempotency_key - assert response is None - - def test_throws_unauthorized_exception( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - mock_audit_log_event, - mock_http_client_with_response, - ): - organization_id = "org_123456789" - - mock_http_client_with_response( - module_instance._http_client, - {"message": "Unauthorized"}, - 401, - {"X-Request-ID": "a-request-id"}, - ) - - with pytest.raises(AuthenticationException) as excinfo: - syncify( - module_instance.create_event( - organization_id=organization_id, event=mock_audit_log_event - ) - ) - assert "(message=Unauthorized, request_id=a-request-id)" == str( - excinfo.value - ) - - def test_throws_badrequest_excpetion( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - mock_audit_log_event, - mock_http_client_with_response, - ): - organization_id = "org_123456789" - - mock_http_client_with_response( - module_instance._http_client, - { - "message": "Audit Log could not be processed due to missing or incorrect data.", - "code": "invalid_audit_log", - "errors": ["error in a field"], - }, - 400, - ) - - with pytest.raises(BadRequestException) as excinfo: - syncify( - module_instance.create_event( - organization_id=organization_id, event=mock_audit_log_event - ) - ) - assert excinfo.value.code == "invalid_audit_log" - assert excinfo.value.errors == ["error in a field"] - assert ( - excinfo.value.message - == "Audit Log could not be processed due to missing or incorrect data." - ) - - class TestCreateExport: - def test_succeeds( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - mock_http_client_with_response, - ): - organization_id = "org_123456789" - now = datetime.now().isoformat() - range_start = now - range_end = now - - expected_payload = { - "object": "audit_log_export", - "id": "audit_log_export_1234", - "state": "pending", - "url": None, - "created_at": now, - "updated_at": now, - } - - mock_http_client_with_response( - module_instance._http_client, expected_payload, 201 - ) - - response = syncify( - module_instance.create_export( - organization_id=organization_id, - range_start=range_start, - range_end=range_end, - ) - ) - - assert response.dict() == expected_payload - - def test_succeeds_with_additional_filters( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - capture_and_mock_http_client_request, - ): - now = datetime.now().isoformat() - organization_id = "org_123456789" - range_start = now - range_end = now - actions = ["foo", "bar"] - actor_names = ["Jon", "Smith"] - actor_ids = ["user_foo", "user_bar"] - targets = ["user", "team"] - - expected_payload = { - "object": "audit_log_export", - "id": "audit_log_export_1234", - "state": "pending", - "url": None, - "created_at": now, - "updated_at": now, - } - - request_kwargs = capture_and_mock_http_client_request( - module_instance._http_client, expected_payload, 201 - ) - - response = syncify( - module_instance.create_export( - actions=actions, - organization_id=organization_id, - range_end=range_end, - range_start=range_start, - targets=targets, - actor_names=actor_names, - actor_ids=actor_ids, - ) - ) - - assert request_kwargs["url"].endswith("/audit_logs/exports") - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == { - "actions": actions, - "organization_id": organization_id, - "range_end": range_end, - "range_start": range_start, - "targets": targets, - "actor_names": actor_names, - "actor_ids": actor_ids, - } - assert response.dict() == expected_payload - - def test_throws_unauthorized_excpetion( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - mock_http_client_with_response, - ): - organization_id = "org_123456789" - range_start = datetime.now().isoformat() - range_end = datetime.now().isoformat() - - mock_http_client_with_response( - module_instance._http_client, - {"message": "Unauthorized"}, - 401, - {"X-Request-ID": "a-request-id"}, - ) - - with pytest.raises(AuthenticationException) as excinfo: - syncify( - module_instance.create_export( - organization_id=organization_id, - range_start=range_start, - range_end=range_end, - ) - ) - assert "(message=Unauthorized, request_id=a-request-id)" == str( - excinfo.value - ) - - class TestGetExport: - def test_succeeds( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - capture_and_mock_http_client_request, - ): - now = datetime.now().isoformat() - expected_payload = { - "object": "audit_log_export", - "id": "audit_log_export_1234", - "state": "pending", - "url": None, - "created_at": now, - "updated_at": now, - } - - request_kwargs = capture_and_mock_http_client_request( - module_instance._http_client, expected_payload, 200 - ) - - response = syncify( - module_instance.get_export( - expected_payload["id"], - ) - ) - - assert request_kwargs["url"].endswith( - "/audit_logs/exports/audit_log_export_1234" - ) - assert request_kwargs["method"] == "get" - assert response.dict() == expected_payload - - def test_throws_unauthorized_excpetion( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - mock_http_client_with_response, - ): - mock_http_client_with_response( - module_instance._http_client, - {"message": "Unauthorized"}, - 401, - {"X-Request-ID": "a-request-id"}, - ) - - with pytest.raises(AuthenticationException) as excinfo: - syncify(module_instance.get_export("audit_log_export_1234")) - - assert "(message=Unauthorized, request_id=a-request-id)" == str( - excinfo.value - ) - - class TestCreateSchema: - def test_succeeds( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - capture_and_mock_http_client_request, - ): - action = "user.signed_in" - - expected_payload = { - "object": "audit_log_schema", - "version": 1, - "targets": [{"type": "user"}], - "actor": {"metadata": {"type": "object", "properties": {}}}, - "metadata": None, - "created_at": "2024-10-14T15:09:44.537Z", - } - - request_kwargs = capture_and_mock_http_client_request( - module_instance._http_client, expected_payload, 201 - ) - - response = syncify( - module_instance.create_schema( - action=action, - targets=[{"type": "user"}], - ) - ) - - assert request_kwargs["url"].endswith( - f"/audit_logs/actions/{action}/schemas" - ) - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == {"targets": [{"type": "user"}]} - assert response.version == 1 - assert response.targets[0].type == "user" - - def test_sends_idempotency_key( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - capture_and_mock_http_client_request, - ): - action = "user.signed_in" - idempotency_key = "test_123456789" - - expected_payload = { - "object": "audit_log_schema", - "version": 1, - "targets": [{"type": "user"}], - "actor": {"metadata": {"type": "object", "properties": {}}}, - "created_at": "2024-10-14T15:09:44.537Z", - } - - request_kwargs = capture_and_mock_http_client_request( - module_instance._http_client, expected_payload, 201 - ) - - syncify( - module_instance.create_schema( - action=action, - targets=[{"type": "user"}], - idempotency_key=idempotency_key, - ) - ) - - assert request_kwargs["headers"]["idempotency-key"] == idempotency_key - - def test_with_actor_and_metadata( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - capture_and_mock_http_client_request, - ): - action = "user.viewed_invoice" - - # Response from API uses full JSON Schema format - expected_payload = { - "object": "audit_log_schema", - "version": 1, - "targets": [ - { - "type": "invoice", - "metadata": { - "type": "object", - "properties": {"status": {"type": "string"}}, - }, - } - ], - "actor": { - "metadata": { - "type": "object", - "properties": {"role": {"type": "string"}}, - } - }, - "metadata": { - "type": "object", - "properties": {"transactionId": {"type": "string"}}, - }, - "created_at": "2024-10-14T15:09:44.537Z", - } - - request_kwargs = capture_and_mock_http_client_request( - module_instance._http_client, expected_payload, 201 - ) - - # Input uses simplified format (like JS SDK) - response = syncify( - module_instance.create_schema( - action=action, - targets=[ - { - "type": "invoice", - "metadata": {"status": "string"}, - } - ], - actor={"metadata": {"role": "string"}}, - metadata={"transactionId": "string"}, - ) - ) - - # Verify serialization transforms to full JSON Schema format - assert request_kwargs["json"]["actor"] == { - "metadata": { - "type": "object", - "properties": {"role": {"type": "string"}}, - } - } - assert request_kwargs["json"]["metadata"] == { - "type": "object", - "properties": {"transactionId": {"type": "string"}}, - } - assert request_kwargs["json"]["targets"] == [ - { - "type": "invoice", - "metadata": { - "type": "object", - "properties": {"status": {"type": "string"}}, - }, - } - ] - assert response.metadata is not None - - def test_without_actor_in_response( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - capture_and_mock_http_client_request, - ): - """Test that schema can be parsed when actor is not in the response.""" - action = "user.signed_in" - - # Response without actor field (backend omits when no custom metadata) - expected_payload = { - "object": "audit_log_schema", - "version": 1, - "targets": [{"type": "user"}], - "metadata": None, - "created_at": "2024-10-14T15:09:44.537Z", - } - - capture_and_mock_http_client_request( - module_instance._http_client, expected_payload, 201 - ) - - response = syncify( - module_instance.create_schema( - action=action, - targets=[{"type": "user"}], - ) - ) - - assert response.version == 1 - assert response.actor is None - - def test_throws_unauthorized_exception( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - mock_http_client_with_response, - ): - mock_http_client_with_response( - module_instance._http_client, - {"message": "Unauthorized"}, - 401, - {"X-Request-ID": "a-request-id"}, - ) - - with pytest.raises(AuthenticationException) as excinfo: - syncify( - module_instance.create_schema( - action="user.signed_in", - targets=[{"type": "user"}], - ) - ) - - assert "(message=Unauthorized, request_id=a-request-id)" == str( - excinfo.value - ) - - class TestListSchemas: - def test_succeeds( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - capture_and_mock_http_client_request, - ): - action = "user.viewed_invoice" - - expected_payload = { - "object": "list", - "data": [ - { - "version": 1, - "actor": { - "metadata": { - "type": "object", - "properties": {"role": {"type": "string"}}, - } - }, - "targets": [ - { - "type": "invoice", - "metadata": { - "type": "object", - "properties": {"status": {"type": "string"}}, - }, - } - ], - "metadata": { - "type": "object", - "properties": {"transactionId": {"type": "string"}}, - }, - "updated_at": "2021-06-25T19:07:33.155Z", - } - ], - "list_metadata": {"before": None, "after": None}, - } - - request_kwargs = capture_and_mock_http_client_request( - module_instance._http_client, expected_payload, 200 - ) - - response = syncify(module_instance.list_schemas(action=action)) - - assert request_kwargs["url"].endswith( - f"/audit_logs/actions/{action}/schemas" - ) - assert request_kwargs["method"] == "get" - assert len(response.data) == 1 - assert response.data[0].version == 1 - - def test_with_pagination_params( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - capture_and_mock_http_client_request, - ): - action = "user.signed_in" - - expected_payload = { - "object": "list", - "data": [], - "list_metadata": {"before": None, "after": None}, - } - - request_kwargs = capture_and_mock_http_client_request( - module_instance._http_client, expected_payload, 200 - ) - - syncify( - module_instance.list_schemas( - action=action, - limit=5, - order="asc", - ) - ) - - assert request_kwargs["params"]["limit"] == 5 - assert request_kwargs["params"]["order"] == "asc" - - class TestListActions: - def test_succeeds( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - capture_and_mock_http_client_request, - ): - expected_payload = { - "object": "list", - "data": [ - { - "object": "audit_log_action", - "name": "user.viewed_invoice", - "schema": { - "object": "audit_log_schema", - "version": 1, - "actor": { - "metadata": { - "type": "object", - "properties": {"role": {"type": "string"}}, - } - }, - "targets": [ - { - "type": "invoice", - "metadata": { - "type": "object", - "properties": {"status": {"type": "string"}}, - }, - } - ], - "metadata": { - "type": "object", - "properties": {"transactionId": {"type": "string"}}, - }, - "updated_at": "2021-06-25T19:07:33.155Z", - }, - "created_at": "2021-06-25T19:07:33.155Z", - "updated_at": "2021-06-25T19:07:33.155Z", - } - ], - "list_metadata": {"before": None, "after": None}, - } - - request_kwargs = capture_and_mock_http_client_request( - module_instance._http_client, expected_payload, 200 - ) - - response = syncify(module_instance.list_actions()) - - assert request_kwargs["url"].endswith("/audit_logs/actions") - assert request_kwargs["method"] == "get" - assert len(response.data) == 1 - assert response.data[0].name == "user.viewed_invoice" - assert response.data[0].schema.version == 1 - - def test_with_pagination_params( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - capture_and_mock_http_client_request, - ): - expected_payload = { - "object": "list", - "data": [], - "list_metadata": {"before": None, "after": None}, - } - - request_kwargs = capture_and_mock_http_client_request( - module_instance._http_client, expected_payload, 200 - ) - - syncify( - module_instance.list_actions( - limit=10, - order="asc", - after="cursor_123", - ) - ) - - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "asc" - assert request_kwargs["params"]["after"] == "cursor_123" - - class TestGetRetention: - def test_succeeds( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - capture_and_mock_http_client_request, - ): - organization_id = "org_123456789" - - expected_payload = {"retention_period_in_days": 30} - - request_kwargs = capture_and_mock_http_client_request( - module_instance._http_client, expected_payload, 200 - ) - - response = syncify(module_instance.get_retention(organization_id)) - - assert request_kwargs["url"].endswith( - f"/organizations/{organization_id}/audit_logs_retention" - ) - assert request_kwargs["method"] == "get" - assert response.retention_period_in_days == 30 - - def test_succeeds_with_null_retention( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - capture_and_mock_http_client_request, - ): - """Test that retention can be parsed when retention_period_in_days is null.""" - organization_id = "org_123456789" - - expected_payload = {"retention_period_in_days": None} - - capture_and_mock_http_client_request( - module_instance._http_client, expected_payload, 200 - ) - - response = syncify(module_instance.get_retention(organization_id)) - - assert response.retention_period_in_days is None - - def test_throws_unauthorized_exception( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - mock_http_client_with_response, - ): - mock_http_client_with_response( - module_instance._http_client, - {"message": "Unauthorized"}, - 401, - {"X-Request-ID": "a-request-id"}, - ) - - with pytest.raises(AuthenticationException) as excinfo: - syncify(module_instance.get_retention("org_123456789")) - - assert "(message=Unauthorized, request_id=a-request-id)" == str( - excinfo.value - ) - - class TestSetRetention: - def test_succeeds( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - capture_and_mock_http_client_request, - ): - organization_id = "org_123456789" - - expected_payload = {"retention_period_in_days": 365} - - request_kwargs = capture_and_mock_http_client_request( - module_instance._http_client, expected_payload, 200 - ) - - response = syncify( - module_instance.set_retention( - organization_id=organization_id, - retention_period_in_days=365, - ) - ) - - assert request_kwargs["url"].endswith( - f"/organizations/{organization_id}/audit_logs_retention" - ) - assert request_kwargs["method"] == "put" - assert request_kwargs["json"] == {"retention_period_in_days": 365} - assert response.retention_period_in_days == 365 - - def test_throws_unauthorized_exception( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - mock_http_client_with_response, - ): - mock_http_client_with_response( - module_instance._http_client, - {"message": "Unauthorized"}, - 401, - {"X-Request-ID": "a-request-id"}, - ) - - with pytest.raises(AuthenticationException) as excinfo: - syncify( - module_instance.set_retention( - organization_id="org_123456789", - retention_period_in_days=30, - ) - ) - - assert "(message=Unauthorized, request_id=a-request-id)" == str( - excinfo.value - ) - - class TestGetConfiguration: - def test_succeeds_with_log_stream( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - capture_and_mock_http_client_request, - ): - organization_id = "org_123456789" - - expected_payload = { - "organization_id": organization_id, - "retention_period_in_days": 30, - "state": "active", - "log_stream": { - "id": "audit_log_stream_01HQJW5XBQZ8Y4R9S3T5V6W7X8", - "type": "Datadog", - "state": "active", - "last_synced_at": "2024-01-15T10:30:00.000Z", - "created_at": "2024-01-15T10:30:00.000Z", - }, - } - - request_kwargs = capture_and_mock_http_client_request( - module_instance._http_client, expected_payload, 200 - ) - - response = syncify(module_instance.get_configuration(organization_id)) - - assert request_kwargs["url"].endswith( - f"/organizations/{organization_id}/audit_log_configuration" - ) - assert request_kwargs["method"] == "get" - assert response.organization_id == organization_id - assert response.retention_period_in_days == 30 - assert response.state == "active" - assert response.log_stream is not None - assert response.log_stream.type == "Datadog" - assert response.log_stream.state == "active" - - def test_succeeds_without_log_stream( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - capture_and_mock_http_client_request, - ): - organization_id = "org_123456789" - - expected_payload = { - "organization_id": organization_id, - "retention_period_in_days": 30, - "state": "inactive", - } - - capture_and_mock_http_client_request( - module_instance._http_client, expected_payload, 200 - ) - - response = syncify(module_instance.get_configuration(organization_id)) - - assert response.organization_id == organization_id - assert response.retention_period_in_days == 30 - assert response.state == "inactive" - assert response.log_stream is None - - def test_throws_unauthorized_exception( - self, - module_instance: Union[AuditLogs, AsyncAuditLogs], - mock_http_client_with_response, - ): - mock_http_client_with_response( - module_instance._http_client, - {"message": "Unauthorized"}, - 401, - {"X-Request-ID": "a-request-id"}, - ) - - with pytest.raises(AuthenticationException) as excinfo: - syncify(module_instance.get_configuration("org_123456789")) - - assert "(message=Unauthorized, request_id=a-request-id)" == str( - excinfo.value - ) diff --git a/tests/test_authorization.py b/tests/test_authorization.py deleted file mode 100644 index 56b0f954..00000000 --- a/tests/test_authorization.py +++ /dev/null @@ -1,494 +0,0 @@ -from typing import Union - -import pytest -from tests.utils.fixtures.mock_environment_role import MockEnvironmentRole -from tests.utils.fixtures.mock_organization_role import MockOrganizationRole -from tests.utils.fixtures.mock_permission import MockPermission -from tests.utils.list_resource import list_response_of -from tests.utils.syncify import syncify -from workos.authorization import AsyncAuthorization, Authorization - - -@pytest.mark.sync_and_async(Authorization, AsyncAuthorization) -class TestAuthorization: - @pytest.fixture(autouse=True) - def setup(self, module_instance: Union[Authorization, AsyncAuthorization]): - self.http_client = module_instance._http_client - self.authorization = module_instance - - @pytest.fixture - def mock_permission(self): - return MockPermission(id="perm_01ABC").dict() - - @pytest.fixture - def mock_permissions(self): - permission_list = [ - MockPermission(id=f"perm_{i}", slug=f"perm-{i}").dict() for i in range(5) - ] - return { - "data": permission_list, - "list_metadata": {"before": None, "after": None}, - "object": "list", - } - - @pytest.fixture - def mock_permissions_multiple_data_pages(self): - permission_list = [ - MockPermission(id=f"perm_{i}", slug=f"perm-{i}").dict() for i in range(40) - ] - return list_response_of(data=permission_list) - - def test_create_permission( - self, mock_permission, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_permission, 201 - ) - - permission = syncify( - self.authorization.create_permission( - slug="documents:read", name="Read Documents" - ) - ) - - assert permission.id == "perm_01ABC" - assert permission.slug == "documents:read" - assert permission.resource_type_slug == "organization" - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith("/authorization/permissions") - assert request_kwargs["json"] == { - "slug": "documents:read", - "name": "Read Documents", - } - - def test_create_permission_with_resource_type_slug( - self, mock_permission, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_permission, 201 - ) - - syncify( - self.authorization.create_permission( - slug="documents:read", - name="Read Documents", - resource_type_slug="project", - ) - ) - - assert request_kwargs["json"] == { - "slug": "documents:read", - "name": "Read Documents", - "resource_type_slug": "project", - } - - def test_create_permission_with_description( - self, mock_permission, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_permission, 201 - ) - - syncify( - self.authorization.create_permission( - slug="documents:read", - name="Read Documents", - description="Allows reading documents", - ) - ) - - assert request_kwargs["json"] == { - "slug": "documents:read", - "name": "Read Documents", - "description": "Allows reading documents", - } - - def test_list_permissions( - self, mock_permissions, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_permissions, 200 - ) - - permissions_response = syncify(self.authorization.list_permissions()) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/authorization/permissions") - assert len(permissions_response.data) == 5 - - def test_list_permissions_auto_pagination( - self, - mock_permissions_multiple_data_pages, - test_auto_pagination, - ): - test_auto_pagination( - http_client=self.http_client, - list_function=self.authorization.list_permissions, - expected_all_page_data=mock_permissions_multiple_data_pages["data"], - ) - - def test_get_permission( - self, mock_permission, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_permission, 200 - ) - - permission = syncify(self.authorization.get_permission("documents:read")) - - assert permission.id == "perm_01ABC" - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith( - "/authorization/permissions/documents:read" - ) - - def test_update_permission( - self, mock_permission, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_permission, 200 - ) - - permission = syncify( - self.authorization.update_permission("documents:read", name="Updated Name") - ) - - assert permission.id == "perm_01ABC" - assert request_kwargs["method"] == "patch" - assert request_kwargs["url"].endswith( - "/authorization/permissions/documents:read" - ) - assert request_kwargs["json"] == {"name": "Updated Name"} - - def test_update_permission_with_description( - self, mock_permission, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_permission, 200 - ) - - syncify( - self.authorization.update_permission( - "documents:read", - name="Updated Name", - description="Updated description", - ) - ) - - assert request_kwargs["json"] == { - "name": "Updated Name", - "description": "Updated description", - } - - def test_delete_permission(self, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, - 202, - headers={"content-type": "text/plain; charset=utf-8"}, - ) - - response = syncify(self.authorization.delete_permission("documents:read")) - - assert response is None - assert request_kwargs["method"] == "delete" - assert request_kwargs["url"].endswith( - "/authorization/permissions/documents:read" - ) - - # --- Organization Role fixtures --- - - @pytest.fixture - def mock_organization_role(self): - return MockOrganizationRole(id="role_01ABC").dict() - - @pytest.fixture - def mock_organization_roles(self): - return { - "data": [MockOrganizationRole(id=f"role_{i}").dict() for i in range(5)], - "object": "list", - } - - # --- Organization Role tests --- - - def test_create_organization_role( - self, mock_organization_role, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_organization_role, 201 - ) - - role = syncify( - self.authorization.create_organization_role( - "org_01EHT88Z8J8795GZNQ4ZP1J81T", - slug="admin", - name="Admin", - ) - ) - - assert role.id == "role_01ABC" - assert role.type == "OrganizationRole" - assert role.resource_type_slug == "organization" - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith( - "/authorization/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/roles" - ) - assert request_kwargs["json"] == {"slug": "admin", "name": "Admin"} - - def test_list_organization_roles( - self, mock_organization_roles, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_organization_roles, 200 - ) - - roles_response = syncify( - self.authorization.list_organization_roles("org_01EHT88Z8J8795GZNQ4ZP1J81T") - ) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith( - "/authorization/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/roles" - ) - assert len(roles_response.data) == 5 - - def test_get_organization_role( - self, mock_organization_role, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_organization_role, 200 - ) - - role = syncify( - self.authorization.get_organization_role( - "org_01EHT88Z8J8795GZNQ4ZP1J81T", "admin" - ) - ) - - assert role.id == "role_01ABC" - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith( - "/authorization/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/roles/admin" - ) - - def test_update_organization_role( - self, mock_organization_role, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_organization_role, 200 - ) - - role = syncify( - self.authorization.update_organization_role( - "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "admin", - name="Super Admin", - ) - ) - - assert role.id == "role_01ABC" - assert request_kwargs["method"] == "patch" - assert request_kwargs["url"].endswith( - "/authorization/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/roles/admin" - ) - assert request_kwargs["json"] == {"name": "Super Admin"} - - def test_set_organization_role_permissions( - self, mock_organization_role, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_organization_role, 200 - ) - - role = syncify( - self.authorization.set_organization_role_permissions( - "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "admin", - permissions=["documents:read", "documents:write"], - ) - ) - - assert role.id == "role_01ABC" - assert request_kwargs["method"] == "put" - assert request_kwargs["url"].endswith( - "/authorization/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/roles/admin/permissions" - ) - assert request_kwargs["json"] == { - "permissions": ["documents:read", "documents:write"] - } - - def test_add_organization_role_permission( - self, mock_organization_role, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_organization_role, 200 - ) - - role = syncify( - self.authorization.add_organization_role_permission( - "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "admin", - permission_slug="documents:read", - ) - ) - - assert role.id == "role_01ABC" - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith( - "/authorization/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/roles/admin/permissions" - ) - assert request_kwargs["json"] == {"slug": "documents:read"} - - def test_remove_organization_role_permission( - self, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, - 202, - headers={"content-type": "text/plain; charset=utf-8"}, - ) - - response = syncify( - self.authorization.remove_organization_role_permission( - "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "admin", - permission_slug="documents:read", - ) - ) - - assert response is None - assert request_kwargs["method"] == "delete" - assert request_kwargs["url"].endswith( - "/authorization/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/roles/admin/permissions/documents:read" - ) - - # --- Environment Role fixtures --- - - @pytest.fixture - def mock_environment_role(self): - return MockEnvironmentRole(id="role_01DEF").dict() - - @pytest.fixture - def mock_environment_roles(self): - return { - "data": [MockEnvironmentRole(id=f"role_{i}").dict() for i in range(5)], - "object": "list", - } - - # --- Environment Role tests --- - - def test_create_environment_role( - self, mock_environment_role, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_environment_role, 201 - ) - - role = syncify( - self.authorization.create_environment_role(slug="member", name="Member") - ) - - assert role.id == "role_01DEF" - assert role.type == "EnvironmentRole" - assert role.resource_type_slug == "organization" - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith("/authorization/roles") - assert request_kwargs["json"] == {"slug": "member", "name": "Member"} - - def test_create_environment_role_with_resource_type_slug( - self, mock_environment_role, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_environment_role, 201 - ) - - syncify( - self.authorization.create_environment_role( - slug="member", - name="Member", - resource_type_slug="project", - ) - ) - - assert request_kwargs["json"] == { - "slug": "member", - "name": "Member", - "resource_type_slug": "project", - } - - def test_list_environment_roles( - self, mock_environment_roles, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_environment_roles, 200 - ) - - roles_response = syncify(self.authorization.list_environment_roles()) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/authorization/roles") - assert len(roles_response.data) == 5 - - def test_get_environment_role( - self, mock_environment_role, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_environment_role, 200 - ) - - role = syncify(self.authorization.get_environment_role("member")) - - assert role.id == "role_01DEF" - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/authorization/roles/member") - - def test_update_environment_role( - self, mock_environment_role, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_environment_role, 200 - ) - - role = syncify( - self.authorization.update_environment_role("member", name="Updated Member") - ) - - assert role.id == "role_01DEF" - assert request_kwargs["method"] == "patch" - assert request_kwargs["url"].endswith("/authorization/roles/member") - assert request_kwargs["json"] == {"name": "Updated Member"} - - def test_set_environment_role_permissions( - self, mock_environment_role, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_environment_role, 200 - ) - - role = syncify( - self.authorization.set_environment_role_permissions( - "member", permissions=["documents:read"] - ) - ) - - assert role.id == "role_01DEF" - assert request_kwargs["method"] == "put" - assert request_kwargs["url"].endswith("/authorization/roles/member/permissions") - assert request_kwargs["json"] == {"permissions": ["documents:read"]} - - def test_add_environment_role_permission( - self, mock_environment_role, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_environment_role, 200 - ) - - role = syncify( - self.authorization.add_environment_role_permission( - "member", permission_slug="documents:read" - ) - ) - - assert role.id == "role_01DEF" - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith("/authorization/roles/member/permissions") - assert request_kwargs["json"] == {"slug": "documents:read"} diff --git a/tests/test_authorization_check.py b/tests/test_authorization_check.py deleted file mode 100644 index 2cc19e6c..00000000 --- a/tests/test_authorization_check.py +++ /dev/null @@ -1,142 +0,0 @@ -from typing import Union - -import pytest -from tests.utils.syncify import syncify -from workos.authorization import AsyncAuthorization, Authorization -from workos.types.authorization.resource_identifier import ( - ResourceIdentifierByExternalId, - ResourceIdentifierById, -) - -MOCK_ORG_MEMBERSHIP_ID = "org_membership_01ABC" -MOCK_PERMISSION_SLUG = "document:read" -MOCK_RESOURCE_ID = "res_01ABC" -MOCK_RESOURCE_TYPE = "document" -MOCK_EXTERNAL_ID = "ext_123" - - -@pytest.mark.sync_and_async(Authorization, AsyncAuthorization) -class TestAuthorizationCheck: - @pytest.fixture(autouse=True) - def setup(self, module_instance: Union[Authorization, AsyncAuthorization]): - self.http_client = module_instance._http_client - self.authorization = module_instance - - def test_check_authorized_by_resource_id( - self, capture_and_mock_http_client_request - ): - mock_response = {"authorized": True} - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_response, 200 - ) - - resource: ResourceIdentifierById = {"resource_id": MOCK_RESOURCE_ID} - response = syncify( - self.authorization.check( - MOCK_ORG_MEMBERSHIP_ID, - permission_slug=MOCK_PERMISSION_SLUG, - resource=resource, - ) - ) - - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith( - f"/authorization/organization_memberships/{MOCK_ORG_MEMBERSHIP_ID}/check" - ) - assert request_kwargs["json"] == { - "permission_slug": MOCK_PERMISSION_SLUG, - "resource_id": MOCK_RESOURCE_ID, - } - - assert response.authorized is True - - def test_check_authorized_by_external_id( - self, capture_and_mock_http_client_request - ): - mock_response = {"authorized": True} - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_response, 200 - ) - - resource: ResourceIdentifierByExternalId = { - "resource_external_id": MOCK_EXTERNAL_ID, - "resource_type_slug": MOCK_RESOURCE_TYPE, - } - response = syncify( - self.authorization.check( - MOCK_ORG_MEMBERSHIP_ID, - permission_slug=MOCK_PERMISSION_SLUG, - resource=resource, - ) - ) - - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith( - f"/authorization/organization_memberships/{MOCK_ORG_MEMBERSHIP_ID}/check" - ) - assert request_kwargs["json"] == { - "permission_slug": MOCK_PERMISSION_SLUG, - "resource_external_id": MOCK_EXTERNAL_ID, - "resource_type_slug": MOCK_RESOURCE_TYPE, - } - - assert response.authorized is True - - def test_check_not_authorized_by_resource_id( - self, capture_and_mock_http_client_request - ): - mock_response = {"authorized": False} - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_response, 200 - ) - - resource: ResourceIdentifierById = {"resource_id": MOCK_RESOURCE_ID} - response = syncify( - self.authorization.check( - MOCK_ORG_MEMBERSHIP_ID, - permission_slug=MOCK_PERMISSION_SLUG, - resource=resource, - ) - ) - - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith( - f"/authorization/organization_memberships/{MOCK_ORG_MEMBERSHIP_ID}/check" - ) - assert request_kwargs["json"] == { - "permission_slug": MOCK_PERMISSION_SLUG, - "resource_id": MOCK_RESOURCE_ID, - } - - assert response.authorized is False - - def test_check_not_authorized_by_external_id( - self, capture_and_mock_http_client_request - ): - mock_response = {"authorized": False} - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_response, 200 - ) - - resource: ResourceIdentifierByExternalId = { - "resource_external_id": MOCK_EXTERNAL_ID, - "resource_type_slug": MOCK_RESOURCE_TYPE, - } - response = syncify( - self.authorization.check( - MOCK_ORG_MEMBERSHIP_ID, - permission_slug=MOCK_PERMISSION_SLUG, - resource=resource, - ) - ) - - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith( - f"/authorization/organization_memberships/{MOCK_ORG_MEMBERSHIP_ID}/check" - ) - assert request_kwargs["json"] == { - "permission_slug": MOCK_PERMISSION_SLUG, - "resource_external_id": MOCK_EXTERNAL_ID, - "resource_type_slug": MOCK_RESOURCE_TYPE, - } - assert response.authorized is False diff --git a/tests/test_authorization_resource.py b/tests/test_authorization_resource.py deleted file mode 100644 index b08c4e91..00000000 --- a/tests/test_authorization_resource.py +++ /dev/null @@ -1,767 +0,0 @@ -from typing import Union - -import pytest -from tests.utils.fixtures.mock_resource import MockAuthorizationResource -from tests.utils.fixtures.mock_resource_list import MockAuthorizationResourceList -from tests.utils.list_resource import list_response_of -from tests.utils.syncify import syncify -from workos.authorization import AsyncAuthorization, Authorization - - -@pytest.mark.sync_and_async(Authorization, AsyncAuthorization) -class TestAuthorizationResourceCRUD: - @pytest.fixture(autouse=True) - def setup(self, module_instance: Union[Authorization, AsyncAuthorization]): - self.http_client = module_instance._http_client - self.authorization = module_instance - - @pytest.fixture - def mock_resource(self): - return MockAuthorizationResource().dict() - - @pytest.fixture - def mock_resources_list_two(self): - return MockAuthorizationResourceList().dict() - - @pytest.fixture - def mock_resources_empty_list(self): - return list_response_of(data=[]) - - # --- get_resource --- - - def test_get_resource(self, mock_resource, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 200 - ) - - response = syncify(self.authorization.get_resource("res_01ABC")) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") - - assert response.dict() == MockAuthorizationResource().dict() - - def test_get_resource_without_parent(self, capture_and_mock_http_client_request): - mock_resource = MockAuthorizationResource(parent_resource_id=None).dict() - capture_and_mock_http_client_request(self.http_client, mock_resource, 200) - - response = syncify(self.authorization.get_resource("res_01ABC")) - - assert ( - response.dict() == MockAuthorizationResource(parent_resource_id=None).dict() - ) - - def test_get_resource_without_description( - self, capture_and_mock_http_client_request - ): - mock_resource = MockAuthorizationResource(description=None).dict() - capture_and_mock_http_client_request(self.http_client, mock_resource, 200) - - response = syncify(self.authorization.get_resource("res_01ABC")) - - assert response.dict() == MockAuthorizationResource(description=None).dict() - - def test_get_resource_without_parent_and_description( - self, capture_and_mock_http_client_request - ): - mock_resource = MockAuthorizationResource( - parent_resource_id=None, description=None - ).dict() - capture_and_mock_http_client_request(self.http_client, mock_resource, 200) - - response = syncify(self.authorization.get_resource("res_01ABC")) - - assert ( - response.dict() - == MockAuthorizationResource( - parent_resource_id=None, description=None - ).dict() - ) - - # --- create_resource --- - - def test_create_resource_with_parent_by_id( - self, mock_resource, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 200 - ) - - response = syncify( - self.authorization.create_resource( - external_id="ext_123", - name="Test Resource", - description="A test resource", - resource_type_slug="document", - organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", - parent={"parent_resource_id": "res_01XYZ"}, - ) - ) - - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith("/authorization/resources") - assert request_kwargs["json"] == { - "resource_type_slug": "document", - "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "external_id": "ext_123", - "name": "Test Resource", - "description": "A test resource", - "parent_resource_id": "res_01XYZ", - } - assert "parent_resource_external_id" not in request_kwargs["json"] - assert "parent_resource_type_slug" not in request_kwargs["json"] - - assert response.dict() == MockAuthorizationResource().dict() - - def test_create_resource_with_parent_by_id_no_description( - self, capture_and_mock_http_client_request - ): - mock_resource = MockAuthorizationResource(description=None).dict() - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 200 - ) - - response = syncify( - self.authorization.create_resource( - external_id="ext_123", - name="Test Resource", - description=None, - resource_type_slug="document", - organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", - parent={"parent_resource_id": "res_01XYZ"}, - ) - ) - - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith("/authorization/resources") - assert request_kwargs["json"] == { - "resource_type_slug": "document", - "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "external_id": "ext_123", - "name": "Test Resource", - "parent_resource_id": "res_01XYZ", - } - assert "description" not in request_kwargs["json"] - assert "parent_resource_external_id" not in request_kwargs["json"] - assert "parent_resource_type_slug" not in request_kwargs["json"] - - assert response.dict() == MockAuthorizationResource(description=None).dict() - - def test_create_resource_with_parent_by_external_id( - self, mock_resource, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 200 - ) - - response = syncify( - self.authorization.create_resource( - external_id="ext_123", - name="Test Resource", - description="A test resource", - resource_type_slug="document", - organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", - parent={ - "parent_resource_external_id": "parent_ext_456", - "parent_resource_type_slug": "folder", - }, - ) - ) - - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith("/authorization/resources") - assert request_kwargs["json"] == { - "resource_type_slug": "document", - "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "external_id": "ext_123", - "name": "Test Resource", - "description": "A test resource", - "parent_resource_external_id": "parent_ext_456", - "parent_resource_type_slug": "folder", - } - - assert "parent_resource_id" not in request_kwargs["json"] - - assert response.dict() == MockAuthorizationResource().dict() - - def test_create_resource_with_parent_by_external_id_no_description( - self, capture_and_mock_http_client_request - ): - mock_resource = MockAuthorizationResource(description=None).dict() - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 200 - ) - - response = syncify( - self.authorization.create_resource( - external_id="ext_123", - name="Test Resource", - description=None, - resource_type_slug="document", - organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", - parent={ - "parent_resource_external_id": "parent_ext_456", - "parent_resource_type_slug": "folder", - }, - ) - ) - - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == { - "resource_type_slug": "document", - "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "external_id": "ext_123", - "name": "Test Resource", - "parent_resource_external_id": "parent_ext_456", - "parent_resource_type_slug": "folder", - } - - assert "description" not in request_kwargs["json"] - assert "parent_resource_id" not in request_kwargs["json"] - - assert response.dict() == MockAuthorizationResource(description=None).dict() - - def test_create_resource_without_parent(self, capture_and_mock_http_client_request): - mock_resource = MockAuthorizationResource(parent_resource_id=None).dict() - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 200 - ) - - response = syncify( - self.authorization.create_resource( - external_id="ext_123", - name="Test Resource", - description="A test resource", - resource_type_slug="document", - organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", - ) - ) - - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == { - "resource_type_slug": "document", - "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "external_id": "ext_123", - "name": "Test Resource", - "description": "A test resource", - } - - assert ( - response.dict() == MockAuthorizationResource(parent_resource_id=None).dict() - ) - - # --- update_resource --- - def test_update_resource_name_only( - self, mock_resource, capture_and_mock_http_client_request - ): - updated_resource = MockAuthorizationResource( - name="New Name", - ).dict() - request_kwargs = capture_and_mock_http_client_request( - self.http_client, updated_resource, 200 - ) - - response = syncify( - self.authorization.update_resource("res_01ABC", name="New Name") - ) - - assert request_kwargs["method"] == "patch" - assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") - assert request_kwargs["json"] == {"name": "New Name"} - - assert ( - response.dict() - == MockAuthorizationResource( - name="New Name", - ).dict() - ) - - def test_update_resource_description_only( - self, mock_resource, capture_and_mock_http_client_request - ): - updated_resource = MockAuthorizationResource( - description="Updated description only", - ).dict() - request_kwargs = capture_and_mock_http_client_request( - self.http_client, updated_resource, 200 - ) - - response = syncify( - self.authorization.update_resource( - "res_01ABC", description="Updated description only" - ) - ) - - assert request_kwargs["method"] == "patch" - assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") - assert request_kwargs["json"] == { - "description": "Updated description only", - } - assert ( - response.dict() - == MockAuthorizationResource( - description="Updated description only", - ).dict() - ) - - def test_update_resource_remove_description( - self, mock_resource, capture_and_mock_http_client_request - ): - updated_resource = MockAuthorizationResource(description=None).dict() - request_kwargs = capture_and_mock_http_client_request( - self.http_client, updated_resource, 200 - ) - - response = syncify( - self.authorization.update_resource("res_01ABC", description=None) - ) - - assert request_kwargs["method"] == "patch" - assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") - assert request_kwargs["json"] == {"description": None} - assert ( - response.dict() - == MockAuthorizationResource( - description=None, - ).dict() - ) - - def test_update_resource_with_name_and_description( - self, capture_and_mock_http_client_request - ): - updated_resource = MockAuthorizationResource( - name="Updated Name", - description="Updated description", - ).dict() - request_kwargs = capture_and_mock_http_client_request( - self.http_client, updated_resource, 200 - ) - - response = syncify( - self.authorization.update_resource( - "res_01ABC", - name="Updated Name", - description="Updated description", - ) - ) - - assert request_kwargs["method"] == "patch" - assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") - assert request_kwargs["json"] == { - "name": "Updated Name", - "description": "Updated description", - } - assert ( - response.dict() - == MockAuthorizationResource( - name="Updated Name", - description="Updated description", - ).dict() - ) - - # --- delete_resource --- - - def test_delete_resource_without_cascade( - self, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, - status_code=204, - ) - - response = syncify(self.authorization.delete_resource("res_01ABC")) - - assert request_kwargs["method"] == "delete" - assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") - assert request_kwargs.get("params") is None - assert response is None - - def test_delete_resource_with_cascade_true( - self, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, - status_code=204, - ) - - response = syncify( - self.authorization.delete_resource("res_01ABC", cascade_delete=True) - ) - - assert request_kwargs["method"] == "delete" - assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") - assert request_kwargs["params"] == {"cascade_delete": "true"} - assert response is None - - def test_delete_resource_with_cascade_false( - self, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, - status_code=204, - ) - - response = syncify( - self.authorization.delete_resource("res_01ABC", cascade_delete=False) - ) - - assert request_kwargs["method"] == "delete" - assert request_kwargs["url"].endswith("/authorization/resources/res_01ABC") - assert request_kwargs["params"] == {"cascade_delete": "false"} - assert response is None - - # --- list_resources --- - def test_list_resources_returns_paginated_list( - self, - mock_resources_list_two, - capture_and_mock_http_client_request, - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list_two, 200 - ) - - response = syncify(self.authorization.list_resources()) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/authorization/resources") - assert request_kwargs["params"] == {"limit": 10, "order": "desc"} - - assert response.object == "list" - assert len(response.data) == 2 - - assert response.data[0].object == "authorization_resource" - assert response.data[0].id == "authz_resource_01HXYZ123ABC456DEF789ABC" - assert response.data[0].external_id == "doc-12345678" - assert response.data[0].name == "Q5 Budget Report" - assert response.data[0].description == "Financial report for Q5 2025" - assert response.data[0].resource_type_slug == "document" - assert response.data[0].organization_id == "org_01HXYZ123ABC456DEF789ABC" - assert ( - response.data[0].parent_resource_id - == "authz_resource_01HXYZ123ABC456DEF789XYZ" - ) - assert response.data[0].created_at == "2024-01-15T09:30:00.000Z" - assert response.data[0].updated_at == "2024-01-15T09:30:00.000Z" - - assert response.data[1].object == "authorization_resource" - assert response.data[1].id == "authz_resource_01HXYZ123ABC456DEF789DEF" - assert response.data[1].external_id == "folder-123" - assert response.data[1].name == "Finance Folder" - assert response.data[1].description is None - assert response.data[1].resource_type_slug == "folder" - assert response.data[1].organization_id == "org_01HXYZ123ABC456DEF789ABC" - assert response.data[1].parent_resource_id is None - assert response.data[1].created_at == "2024-01-14T08:00:00.000Z" - assert response.data[1].updated_at == "2024-01-14T08:00:00.000Z" - - assert response.list_metadata.before is None - assert response.list_metadata.after == "authz_resource_01HXYZ123ABC456DEF789DEF" - - def test_list_resources_returns_empty_list( - self, - mock_resources_empty_list, - capture_and_mock_http_client_request, - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_empty_list, 200 - ) - - response = syncify(self.authorization.list_resources()) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/authorization/resources") - assert request_kwargs["params"] == {"limit": 10, "order": "desc"} - - assert len(response.data) == 0 - assert response.list_metadata.before is None - assert response.list_metadata.after is None - - def test_list_resources_request_with_no_parameters( - self, - mock_resources_list_two, - capture_and_mock_http_client_request, - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list_two, 200 - ) - - syncify(self.authorization.list_resources()) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/authorization/resources") - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - assert "organization_id" not in request_kwargs["params"] - assert "resource_type_slug" not in request_kwargs["params"] - assert "parent_resource_id" not in request_kwargs["params"] - assert "parent_resource_type_slug" not in request_kwargs["params"] - assert "parent_external_id" not in request_kwargs["params"] - assert "search" not in request_kwargs["params"] - assert "before" not in request_kwargs["params"] - assert "after" not in request_kwargs["params"] - - def test_list_resources_with_organization_id( - self, mock_resources_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list_two, 200 - ) - - syncify(self.authorization.list_resources(organization_id="org_123")) - - assert request_kwargs["params"]["organization_id"] == "org_123" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - assert "resource_type_slug" not in request_kwargs["params"] - assert "parent_resource_id" not in request_kwargs["params"] - assert "parent_resource_type_slug" not in request_kwargs["params"] - assert "parent_external_id" not in request_kwargs["params"] - assert "search" not in request_kwargs["params"] - assert "before" not in request_kwargs["params"] - assert "after" not in request_kwargs["params"] - - def test_list_resources_with_resource_type_slug( - self, mock_resources_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list_two, 200 - ) - - syncify(self.authorization.list_resources(resource_type_slug="document")) - - assert request_kwargs["params"]["resource_type_slug"] == "document" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - assert "organization_id" not in request_kwargs["params"] - assert "parent_resource_id" not in request_kwargs["params"] - assert "parent_resource_type_slug" not in request_kwargs["params"] - assert "parent_external_id" not in request_kwargs["params"] - assert "search" not in request_kwargs["params"] - assert "before" not in request_kwargs["params"] - assert "after" not in request_kwargs["params"] - - def test_list_resources_with_parent_resource_id( - self, mock_resources_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list_two, 200 - ) - - syncify(self.authorization.list_resources(parent_resource_id="res_parent_123")) - - assert request_kwargs["params"]["parent_resource_id"] == "res_parent_123" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - assert "organization_id" not in request_kwargs["params"] - assert "resource_type_slug" not in request_kwargs["params"] - assert "parent_resource_type_slug" not in request_kwargs["params"] - assert "parent_external_id" not in request_kwargs["params"] - assert "search" not in request_kwargs["params"] - assert "before" not in request_kwargs["params"] - assert "after" not in request_kwargs["params"] - - def test_list_resources_with_parent_resource_type_slug( - self, mock_resources_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list_two, 200 - ) - - syncify(self.authorization.list_resources(parent_resource_type_slug="folder")) - - assert request_kwargs["params"]["parent_resource_type_slug"] == "folder" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - assert "organization_id" not in request_kwargs["params"] - assert "resource_type_slug" not in request_kwargs["params"] - assert "parent_resource_id" not in request_kwargs["params"] - assert "parent_external_id" not in request_kwargs["params"] - assert "search" not in request_kwargs["params"] - assert "before" not in request_kwargs["params"] - assert "after" not in request_kwargs["params"] - - def test_list_resources_with_parent_external_id( - self, mock_resources_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list_two, 200 - ) - - syncify(self.authorization.list_resources(parent_external_id="parent_ext_456")) - - assert request_kwargs["params"]["parent_external_id"] == "parent_ext_456" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - assert "organization_id" not in request_kwargs["params"] - assert "resource_type_slug" not in request_kwargs["params"] - assert "parent_resource_id" not in request_kwargs["params"] - assert "parent_resource_type_slug" not in request_kwargs["params"] - assert "search" not in request_kwargs["params"] - assert "before" not in request_kwargs["params"] - assert "after" not in request_kwargs["params"] - - def test_list_resources_with_search( - self, mock_resources_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list_two, 200 - ) - - syncify(self.authorization.list_resources(search="Budget")) - - assert request_kwargs["params"]["search"] == "Budget" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - assert "organization_id" not in request_kwargs["params"] - assert "resource_type_slug" not in request_kwargs["params"] - assert "parent_resource_id" not in request_kwargs["params"] - assert "parent_resource_type_slug" not in request_kwargs["params"] - assert "parent_external_id" not in request_kwargs["params"] - assert "before" not in request_kwargs["params"] - assert "after" not in request_kwargs["params"] - - def test_list_resources_with_limit( - self, mock_resources_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list_two, 200 - ) - - syncify(self.authorization.list_resources(limit=25)) - - assert request_kwargs["params"]["limit"] == 25 - assert request_kwargs["params"]["order"] == "desc" - - assert "organization_id" not in request_kwargs["params"] - assert "resource_type_slug" not in request_kwargs["params"] - assert "parent_resource_id" not in request_kwargs["params"] - assert "parent_resource_type_slug" not in request_kwargs["params"] - assert "parent_external_id" not in request_kwargs["params"] - assert "search" not in request_kwargs["params"] - assert "before" not in request_kwargs["params"] - assert "after" not in request_kwargs["params"] - - def test_list_resources_with_before( - self, mock_resources_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list_two, 200 - ) - - syncify(self.authorization.list_resources(before="cursor_before")) - - assert request_kwargs["params"]["before"] == "cursor_before" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - assert "organization_id" not in request_kwargs["params"] - assert "resource_type_slug" not in request_kwargs["params"] - assert "parent_resource_id" not in request_kwargs["params"] - assert "parent_resource_type_slug" not in request_kwargs["params"] - assert "parent_external_id" not in request_kwargs["params"] - assert "search" not in request_kwargs["params"] - assert "after" not in request_kwargs["params"] - - def test_list_resources_with_after( - self, mock_resources_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list_two, 200 - ) - - syncify(self.authorization.list_resources(after="cursor_after")) - - assert request_kwargs["params"]["after"] == "cursor_after" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - assert "organization_id" not in request_kwargs["params"] - assert "resource_type_slug" not in request_kwargs["params"] - assert "parent_resource_id" not in request_kwargs["params"] - assert "parent_resource_type_slug" not in request_kwargs["params"] - assert "parent_external_id" not in request_kwargs["params"] - assert "search" not in request_kwargs["params"] - assert "before" not in request_kwargs["params"] - - def test_list_resources_with_order_asc( - self, mock_resources_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list_two, 200 - ) - - syncify(self.authorization.list_resources(order="asc")) - - assert request_kwargs["params"]["order"] == "asc" - assert request_kwargs["params"]["limit"] == 10 - - assert "organization_id" not in request_kwargs["params"] - assert "resource_type_slug" not in request_kwargs["params"] - assert "parent_resource_id" not in request_kwargs["params"] - assert "parent_resource_type_slug" not in request_kwargs["params"] - assert "parent_external_id" not in request_kwargs["params"] - assert "search" not in request_kwargs["params"] - assert "before" not in request_kwargs["params"] - assert "after" not in request_kwargs["params"] - - def test_list_resources_with_order_desc( - self, mock_resources_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list_two, 200 - ) - - syncify(self.authorization.list_resources(order="desc")) - - assert request_kwargs["params"]["order"] == "desc" - assert request_kwargs["params"]["limit"] == 10 - - assert "organization_id" not in request_kwargs["params"] - assert "resource_type_slug" not in request_kwargs["params"] - assert "parent_resource_id" not in request_kwargs["params"] - assert "parent_resource_type_slug" not in request_kwargs["params"] - assert "parent_external_id" not in request_kwargs["params"] - assert "search" not in request_kwargs["params"] - assert "before" not in request_kwargs["params"] - assert "after" not in request_kwargs["params"] - - def test_list_resources_with_all_parameters( - self, mock_resources_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list_two, 200 - ) - - syncify( - self.authorization.list_resources( - organization_id="org_123", - resource_type_slug="document", - parent_resource_id="res_parent_123", - parent_resource_type_slug="folder", - parent_external_id="parent_ext_456", - search="Budget", - limit=5, - before="cursor_before", - after="cursor_after", - order="asc", - ) - ) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/authorization/resources") - assert request_kwargs["params"]["organization_id"] == "org_123" - assert request_kwargs["params"]["resource_type_slug"] == "document" - assert request_kwargs["params"]["parent_resource_id"] == "res_parent_123" - assert request_kwargs["params"]["parent_resource_type_slug"] == "folder" - assert request_kwargs["params"]["parent_external_id"] == "parent_ext_456" - assert request_kwargs["params"]["search"] == "Budget" - assert request_kwargs["params"]["limit"] == 5 - assert request_kwargs["params"]["before"] == "cursor_before" - assert request_kwargs["params"]["after"] == "cursor_after" - assert request_kwargs["params"]["order"] == "asc" diff --git a/tests/test_authorization_resource_external_id.py b/tests/test_authorization_resource_external_id.py deleted file mode 100644 index 6cff6773..00000000 --- a/tests/test_authorization_resource_external_id.py +++ /dev/null @@ -1,297 +0,0 @@ -from typing import Union - -import pytest -from tests.utils.fixtures.mock_resource import MockAuthorizationResource -from tests.utils.syncify import syncify -from workos.authorization import AsyncAuthorization, Authorization - - -MOCK_ORG_ID = "org_01EHT88Z8J8795GZNQ4ZP1J81T" -MOCK_RESOURCE_TYPE = "document" -MOCK_EXTERNAL_ID = "ext_123" - - -@pytest.mark.sync_and_async(Authorization, AsyncAuthorization) -class TestAuthorizationResourceExternalId: - @pytest.fixture(autouse=True) - def setup(self, module_instance: Union[Authorization, AsyncAuthorization]): - self.http_client = module_instance._http_client - self.authorization = module_instance - - @pytest.fixture - def mock_resource(self): - return MockAuthorizationResource().dict() - - # --- get_resource_by_external_id --- - def test_get_resource_by_external_id( - self, mock_resource, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resource, 200 - ) - - response = syncify( - self.authorization.get_resource_by_external_id( - MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID - ) - ) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith( - f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" - ) - - assert response.dict() == MockAuthorizationResource().dict() - - def test_get_resource_by_external_id_without_parent( - self, capture_and_mock_http_client_request - ): - mock_resource = MockAuthorizationResource(parent_resource_id=None).dict() - capture_and_mock_http_client_request(self.http_client, mock_resource, 200) - - response = syncify( - self.authorization.get_resource_by_external_id( - MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID - ) - ) - - assert ( - response.dict() == MockAuthorizationResource(parent_resource_id=None).dict() - ) - - def test_get_resource_by_external_id_without_description( - self, capture_and_mock_http_client_request - ): - mock_resource = MockAuthorizationResource(description=None).dict() - capture_and_mock_http_client_request(self.http_client, mock_resource, 200) - - response = syncify( - self.authorization.get_resource_by_external_id( - MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID - ) - ) - - assert response.dict() == MockAuthorizationResource(description=None).dict() - - def test_get_resource_by_external_id_without_parent_and_description( - self, capture_and_mock_http_client_request - ): - mock_resource = MockAuthorizationResource( - parent_resource_id=None, description=None - ).dict() - capture_and_mock_http_client_request(self.http_client, mock_resource, 200) - - response = syncify( - self.authorization.get_resource_by_external_id( - MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID - ) - ) - - assert ( - response.dict() - == MockAuthorizationResource( - parent_resource_id=None, description=None - ).dict() - ) - - # --- update_resource_by_external_id --- - - def test_update_resource_by_external_id_name_only( - self, mock_resource, capture_and_mock_http_client_request - ): - updated_resource = MockAuthorizationResource(name="New Name").dict() - request_kwargs = capture_and_mock_http_client_request( - self.http_client, updated_resource, 200 - ) - - response = syncify( - self.authorization.update_resource_by_external_id( - MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID, name="New Name" - ) - ) - - assert request_kwargs["method"] == "patch" - assert request_kwargs["url"].endswith( - f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" - ) - assert request_kwargs["json"] == {"name": "New Name"} - - assert ( - response.dict() - == MockAuthorizationResource( - name="New Name", - ).dict() - ) - - def test_update_resource_by_external_id_description_only( - self, capture_and_mock_http_client_request - ): - updated_resource = MockAuthorizationResource( - description="Updated description only", - ).dict() - request_kwargs = capture_and_mock_http_client_request( - self.http_client, updated_resource, 200 - ) - - response = syncify( - self.authorization.update_resource_by_external_id( - MOCK_ORG_ID, - MOCK_RESOURCE_TYPE, - MOCK_EXTERNAL_ID, - description="Updated description only", - ) - ) - - assert request_kwargs["method"] == "patch" - assert request_kwargs["url"].endswith( - f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" - ) - assert request_kwargs["json"] == { - "description": "Updated description only", - } - - assert ( - response.dict() - == MockAuthorizationResource( - description="Updated description only", - ).dict() - ) - - def test_update_resource_by_external_id_name_and_description( - self, capture_and_mock_http_client_request - ): - updated_resource = MockAuthorizationResource( - name="Updated Name", - description="Updated description", - ).dict() - request_kwargs = capture_and_mock_http_client_request( - self.http_client, updated_resource, 200 - ) - - response = syncify( - self.authorization.update_resource_by_external_id( - MOCK_ORG_ID, - MOCK_RESOURCE_TYPE, - MOCK_EXTERNAL_ID, - name="Updated Name", - description="Updated description", - ) - ) - - assert request_kwargs["method"] == "patch" - assert request_kwargs["url"].endswith( - f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" - ) - assert request_kwargs["json"] == { - "name": "Updated Name", - "description": "Updated description", - } - - assert ( - response.dict() - == MockAuthorizationResource( - name="Updated Name", - description="Updated description", - ).dict() - ) - - def test_update_resource_by_external_id_remove_description( - self, capture_and_mock_http_client_request - ): - updated_resource = MockAuthorizationResource(description=None).dict() - request_kwargs = capture_and_mock_http_client_request( - self.http_client, updated_resource, 200 - ) - - response = syncify( - self.authorization.update_resource_by_external_id( - MOCK_ORG_ID, - MOCK_RESOURCE_TYPE, - MOCK_EXTERNAL_ID, - description=None, - ) - ) - - assert request_kwargs["method"] == "patch" - assert request_kwargs["url"].endswith( - f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" - ) - assert request_kwargs["json"] == {"description": None} - - assert ( - response.dict() - == MockAuthorizationResource( - description=None, - ).dict() - ) - - # --- delete_resource_by_external_id --- - - def test_delete_resource_by_external_id_without_cascade( - self, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, - status_code=204, - ) - - response = syncify( - self.authorization.delete_resource_by_external_id( - MOCK_ORG_ID, MOCK_RESOURCE_TYPE, MOCK_EXTERNAL_ID - ) - ) - - assert request_kwargs["method"] == "delete" - assert request_kwargs["url"].endswith( - f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" - ) - assert request_kwargs.get("params") is None - assert response is None - - def test_delete_resource_by_external_id_with_cascade_true( - self, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, - status_code=204, - ) - - response = syncify( - self.authorization.delete_resource_by_external_id( - MOCK_ORG_ID, - MOCK_RESOURCE_TYPE, - MOCK_EXTERNAL_ID, - cascade_delete=True, - ) - ) - - assert request_kwargs["method"] == "delete" - assert request_kwargs["url"].endswith( - f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" - ) - assert request_kwargs["params"] == {"cascade_delete": "true"} - assert response is None - - def test_delete_resource_by_external_id_with_cascade_false( - self, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, - status_code=204, - ) - - response = syncify( - self.authorization.delete_resource_by_external_id( - MOCK_ORG_ID, - MOCK_RESOURCE_TYPE, - MOCK_EXTERNAL_ID, - cascade_delete=False, - ) - ) - - assert request_kwargs["method"] == "delete" - assert request_kwargs["url"].endswith( - f"/authorization/organizations/{MOCK_ORG_ID}/resources/{MOCK_RESOURCE_TYPE}/{MOCK_EXTERNAL_ID}" - ) - assert request_kwargs["params"] == {"cascade_delete": "false"} - assert response is None diff --git a/tests/test_authorization_resource_memberships.py b/tests/test_authorization_resource_memberships.py deleted file mode 100644 index bcaf2107..00000000 --- a/tests/test_authorization_resource_memberships.py +++ /dev/null @@ -1,904 +0,0 @@ -from typing import Union - -import pytest -from tests.utils.fixtures.mock_organization_membership import ( - MockAuthorizationOrganizationMembershipList, -) -from tests.utils.fixtures.mock_resource_list import MockAuthorizationResourceList -from tests.utils.syncify import syncify -from workos.authorization import AsyncAuthorization, Authorization -from workos.types.authorization.parent_resource_identifier import ( - ParentResourceByExternalId, - ParentResourceById, -) - - -@pytest.mark.sync_and_async(Authorization, AsyncAuthorization) -class TestListResourcesForMembership: - @pytest.fixture(autouse=True) - def setup(self, module_instance: Union[Authorization, AsyncAuthorization]): - self.http_client = module_instance._http_client - self.authorization = module_instance - - @pytest.fixture - def mock_resources_list(self): - return MockAuthorizationResourceList().model_dump() - - def test_list_resources_for_membership_with_parent_by_id_returns_paginated_list( - self, mock_resources_list, capture_and_mock_http_client_request - ): - parent = ParentResourceById(parent_resource_id="res_parent_123") - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - response = syncify( - self.authorization.list_resources_for_membership( - "om_01ABC", - permission_slug="document:read", - parent_resource=parent, - ) - ) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith( - "/authorization/organization_memberships/om_01ABC/resources" - ) - assert request_kwargs["params"]["parent_resource_id"] == "res_parent_123" - assert request_kwargs["params"]["permission_slug"] == "document:read" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - assert response.object == "list" - assert len(response.data) == 2 - assert response.data[0].object == "authorization_resource" - assert response.data[0].id == "authz_resource_01HXYZ123ABC456DEF789ABC" - assert response.data[0].external_id == "doc-12345678" - assert response.data[0].name == "Q5 Budget Report" - assert response.data[0].description == "Financial report for Q5 2025" - assert response.data[0].resource_type_slug == "document" - assert response.data[0].organization_id == "org_01HXYZ123ABC456DEF789ABC" - assert ( - response.data[0].parent_resource_id - == "authz_resource_01HXYZ123ABC456DEF789XYZ" - ) - assert response.data[0].created_at == "2024-01-15T09:30:00.000Z" - assert response.data[0].updated_at == "2024-01-15T09:30:00.000Z" - assert response.data[1].object == "authorization_resource" - assert response.data[1].id == "authz_resource_01HXYZ123ABC456DEF789DEF" - assert response.data[1].external_id == "folder-123" - assert response.data[1].name == "Finance Folder" - assert response.data[1].description is None - assert response.data[1].resource_type_slug == "folder" - assert response.data[1].organization_id == "org_01HXYZ123ABC456DEF789ABC" - assert response.data[1].parent_resource_id is None - assert response.data[1].created_at == "2024-01-14T08:00:00.000Z" - assert response.data[1].updated_at == "2024-01-14T08:00:00.000Z" - assert response.list_metadata.before is None - assert response.list_metadata.after == "authz_resource_01HXYZ123ABC456DEF789DEF" - - def test_list_resources_for_membership_with_parent_by_id_with_limit( - self, mock_resources_list, capture_and_mock_http_client_request - ): - parent = ParentResourceById(parent_resource_id="res_parent_123") - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - syncify( - self.authorization.list_resources_for_membership( - "om_01ABC", - permission_slug="document:read", - parent_resource=parent, - limit=25, - ) - ) - - assert request_kwargs["params"]["parent_resource_id"] == "res_parent_123" - assert request_kwargs["params"]["permission_slug"] == "document:read" - assert request_kwargs["params"]["limit"] == 25 - assert request_kwargs["params"]["order"] == "desc" - - def test_list_resources_for_membership_with_parent_by_id_with_before( - self, mock_resources_list, capture_and_mock_http_client_request - ): - parent = ParentResourceById(parent_resource_id="res_parent_123") - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - syncify( - self.authorization.list_resources_for_membership( - "om_01ABC", - permission_slug="document:read", - parent_resource=parent, - before="cursor_before", - ) - ) - - assert request_kwargs["params"]["parent_resource_id"] == "res_parent_123" - assert request_kwargs["params"]["before"] == "cursor_before" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - def test_list_resources_for_membership_with_parent_by_id_with_after( - self, mock_resources_list, capture_and_mock_http_client_request - ): - parent = ParentResourceById(parent_resource_id="res_parent_123") - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - syncify( - self.authorization.list_resources_for_membership( - "om_01ABC", - permission_slug="document:read", - parent_resource=parent, - after="cursor_after", - ) - ) - - assert request_kwargs["params"]["parent_resource_id"] == "res_parent_123" - assert request_kwargs["params"]["after"] == "cursor_after" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - def test_list_resources_for_membership_with_parent_by_id_with_order_asc( - self, mock_resources_list, capture_and_mock_http_client_request - ): - parent = ParentResourceById(parent_resource_id="res_parent_123") - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - syncify( - self.authorization.list_resources_for_membership( - "om_01ABC", - permission_slug="document:read", - parent_resource=parent, - order="asc", - ) - ) - - assert request_kwargs["params"]["parent_resource_id"] == "res_parent_123" - assert request_kwargs["params"]["order"] == "asc" - assert request_kwargs["params"]["limit"] == 10 - - def test_list_resources_for_membership_with_parent_by_id_with_order_desc( - self, mock_resources_list, capture_and_mock_http_client_request - ): - parent = ParentResourceById(parent_resource_id="res_parent_123") - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - syncify( - self.authorization.list_resources_for_membership( - "om_01ABC", - permission_slug="document:read", - parent_resource=parent, - order="desc", - ) - ) - - assert request_kwargs["params"]["parent_resource_id"] == "res_parent_123" - assert request_kwargs["params"]["order"] == "desc" - assert request_kwargs["params"]["limit"] == 10 - - def test_list_resources_for_membership_with_parent_by_id_with_all_parameters( - self, mock_resources_list, capture_and_mock_http_client_request - ): - parent = ParentResourceById(parent_resource_id="res_parent_123") - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - response = syncify( - self.authorization.list_resources_for_membership( - "om_01ABC", - permission_slug="document:read", - parent_resource=parent, - limit=5, - before="cursor_before", - after="cursor_after", - order="asc", - ) - ) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith( - "/authorization/organization_memberships/om_01ABC/resources" - ) - assert request_kwargs["params"]["parent_resource_id"] == "res_parent_123" - assert request_kwargs["params"]["permission_slug"] == "document:read" - assert request_kwargs["params"]["limit"] == 5 - assert request_kwargs["params"]["before"] == "cursor_before" - assert request_kwargs["params"]["after"] == "cursor_after" - assert request_kwargs["params"]["order"] == "asc" - - assert response.object == "list" - assert len(response.data) == 2 - - # --- list_resources_for_membership with ParentResourceByExternalId --- - - def test_list_resources_for_membership_with_parent_by_external_id_returns_paginated_list( - self, mock_resources_list, capture_and_mock_http_client_request - ): - parent = ParentResourceByExternalId( - parent_resource_external_id="parent_ext_456", - parent_resource_type_slug="folder", - ) - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - response = syncify( - self.authorization.list_resources_for_membership( - "om_01ABC", - permission_slug="document:read", - parent_resource=parent, - ) - ) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith( - "/authorization/organization_memberships/om_01ABC/resources" - ) - assert ( - request_kwargs["params"]["parent_resource_external_id"] == "parent_ext_456" - ) - assert request_kwargs["params"]["parent_resource_type_slug"] == "folder" - assert request_kwargs["params"]["permission_slug"] == "document:read" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - assert "parent_resource_id" not in request_kwargs["params"] - - assert response.object == "list" - assert len(response.data) == 2 - assert response.data[0].object == "authorization_resource" - assert response.data[0].id == "authz_resource_01HXYZ123ABC456DEF789ABC" - assert response.data[0].external_id == "doc-12345678" - assert response.data[0].name == "Q5 Budget Report" - assert response.data[0].description == "Financial report for Q5 2025" - assert response.data[0].resource_type_slug == "document" - assert response.data[0].organization_id == "org_01HXYZ123ABC456DEF789ABC" - assert ( - response.data[0].parent_resource_id - == "authz_resource_01HXYZ123ABC456DEF789XYZ" - ) - assert response.data[0].created_at == "2024-01-15T09:30:00.000Z" - assert response.data[0].updated_at == "2024-01-15T09:30:00.000Z" - assert response.data[1].object == "authorization_resource" - assert response.data[1].id == "authz_resource_01HXYZ123ABC456DEF789DEF" - assert response.data[1].external_id == "folder-123" - assert response.data[1].name == "Finance Folder" - assert response.data[1].description is None - assert response.data[1].resource_type_slug == "folder" - assert response.data[1].organization_id == "org_01HXYZ123ABC456DEF789ABC" - assert response.data[1].parent_resource_id is None - assert response.data[1].created_at == "2024-01-14T08:00:00.000Z" - assert response.data[1].updated_at == "2024-01-14T08:00:00.000Z" - assert response.list_metadata.before is None - - def test_list_resources_for_membership_with_parent_by_external_id_with_limit( - self, mock_resources_list, capture_and_mock_http_client_request - ): - parent = ParentResourceByExternalId( - parent_resource_external_id="parent_ext_456", - parent_resource_type_slug="folder", - ) - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - syncify( - self.authorization.list_resources_for_membership( - "om_01ABC", - permission_slug="document:read", - parent_resource=parent, - limit=25, - ) - ) - - assert ( - request_kwargs["params"]["parent_resource_external_id"] == "parent_ext_456" - ) - assert request_kwargs["params"]["parent_resource_type_slug"] == "folder" - assert request_kwargs["params"]["limit"] == 25 - assert request_kwargs["params"]["order"] == "desc" - - def test_list_resources_for_membership_with_parent_by_external_id_with_before( - self, mock_resources_list, capture_and_mock_http_client_request - ): - parent = ParentResourceByExternalId( - parent_resource_external_id="parent_ext_456", - parent_resource_type_slug="folder", - ) - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - syncify( - self.authorization.list_resources_for_membership( - "om_01ABC", - permission_slug="document:read", - parent_resource=parent, - before="cursor_before", - ) - ) - - assert ( - request_kwargs["params"]["parent_resource_external_id"] == "parent_ext_456" - ) - assert request_kwargs["params"]["before"] == "cursor_before" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - def test_list_resources_for_membership_with_parent_by_external_id_with_after( - self, mock_resources_list, capture_and_mock_http_client_request - ): - parent = ParentResourceByExternalId( - parent_resource_external_id="parent_ext_456", - parent_resource_type_slug="folder", - ) - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - syncify( - self.authorization.list_resources_for_membership( - "om_01ABC", - permission_slug="document:read", - parent_resource=parent, - after="cursor_after", - ) - ) - - assert ( - request_kwargs["params"]["parent_resource_external_id"] == "parent_ext_456" - ) - assert request_kwargs["params"]["after"] == "cursor_after" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - def test_list_resources_for_membership_with_parent_by_external_id_with_order_asc( - self, mock_resources_list, capture_and_mock_http_client_request - ): - parent = ParentResourceByExternalId( - parent_resource_external_id="parent_ext_456", - parent_resource_type_slug="folder", - ) - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - syncify( - self.authorization.list_resources_for_membership( - "om_01ABC", - permission_slug="document:read", - parent_resource=parent, - order="asc", - ) - ) - - assert ( - request_kwargs["params"]["parent_resource_external_id"] == "parent_ext_456" - ) - assert request_kwargs["params"]["order"] == "asc" - assert request_kwargs["params"]["limit"] == 10 - - def test_list_resources_for_membership_with_parent_by_external_id_with_order_desc( - self, mock_resources_list, capture_and_mock_http_client_request - ): - parent = ParentResourceByExternalId( - parent_resource_external_id="parent_ext_456", - parent_resource_type_slug="folder", - ) - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - syncify( - self.authorization.list_resources_for_membership( - "om_01ABC", - permission_slug="document:read", - parent_resource=parent, - order="desc", - ) - ) - - assert ( - request_kwargs["params"]["parent_resource_external_id"] == "parent_ext_456" - ) - assert request_kwargs["params"]["order"] == "desc" - assert request_kwargs["params"]["limit"] == 10 - - def test_list_resources_for_membership_with_parent_by_external_id_with_all_parameters( - self, mock_resources_list, capture_and_mock_http_client_request - ): - parent = ParentResourceByExternalId( - parent_resource_external_id="parent_ext_456", - parent_resource_type_slug="folder", - ) - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_resources_list, 200 - ) - - response = syncify( - self.authorization.list_resources_for_membership( - "om_01ABC", - permission_slug="document:read", - parent_resource=parent, - limit=5, - before="cursor_before", - after="cursor_after", - order="asc", - ) - ) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith( - "/authorization/organization_memberships/om_01ABC/resources" - ) - assert ( - request_kwargs["params"]["parent_resource_external_id"] == "parent_ext_456" - ) - assert request_kwargs["params"]["parent_resource_type_slug"] == "folder" - assert request_kwargs["params"]["permission_slug"] == "document:read" - assert request_kwargs["params"]["limit"] == 5 - assert request_kwargs["params"]["before"] == "cursor_before" - assert request_kwargs["params"]["after"] == "cursor_after" - assert request_kwargs["params"]["order"] == "asc" - assert "parent_resource_id" not in request_kwargs["params"] - - assert response.object == "list" - assert len(response.data) == 2 - - -@pytest.mark.sync_and_async(Authorization, AsyncAuthorization) -class TestListMembershipsForResource: - @pytest.fixture(autouse=True) - def setup(self, module_instance: Union[Authorization, AsyncAuthorization]): - self.http_client = module_instance._http_client - self.authorization = module_instance - - @pytest.fixture - def mock_memberships_list_two(self): - return MockAuthorizationOrganizationMembershipList().model_dump() - - # --- list_memberships_for_resource (by resource_id) --- - - def test_list_memberships_for_resource_returns_paginated_list( - self, mock_memberships_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_memberships_list_two, 200 - ) - - response = syncify( - self.authorization.list_memberships_for_resource( - "authz_resource_01HXYZ123ABC456DEF789ABC", - permission_slug="document:read", - ) - ) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith( - "/authorization/resources/authz_resource_01HXYZ123ABC456DEF789ABC/organization_memberships" - ) - assert request_kwargs["params"]["permission_slug"] == "document:read" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - assert response.object == "list" - assert len(response.data) == 2 - assert response.data[0].object == "organization_membership" - assert response.data[0].id == "om_01ABC" - assert response.data[0].user_id == "user_123" - assert response.data[0].organization_id == "org_456" - assert response.data[0].status == "active" - assert response.data[0].created_at == "2024-01-01T00:00:00Z" - assert response.data[0].updated_at == "2024-01-01T00:00:00Z" - assert response.data[1].object == "organization_membership" - assert response.data[1].id == "om_01DEF" - assert response.data[1].user_id == "user_789" - assert response.data[1].organization_id == "org_456" - assert response.data[1].status == "active" - assert response.data[1].created_at == "2024-01-02T00:00:00Z" - assert response.data[1].updated_at == "2024-01-02T00:00:00Z" - assert response.list_metadata.before is None - assert response.list_metadata.after == "om_01DEF" - - def test_list_memberships_for_resource_with_assignment_direct( - self, mock_memberships_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_memberships_list_two, 200 - ) - - syncify( - self.authorization.list_memberships_for_resource( - "authz_resource_01HXYZ", - permission_slug="document:read", - assignment="direct", - ) - ) - - assert request_kwargs["params"]["permission_slug"] == "document:read" - assert request_kwargs["params"]["assignment"] == "direct" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - def test_list_memberships_for_resource_with_assignment_indirect( - self, mock_memberships_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_memberships_list_two, 200 - ) - - syncify( - self.authorization.list_memberships_for_resource( - "authz_resource_01HXYZ", - permission_slug="document:read", - assignment="indirect", - ) - ) - - assert request_kwargs["params"]["assignment"] == "indirect" - assert request_kwargs["params"]["permission_slug"] == "document:read" - - def test_list_memberships_for_resource_with_limit( - self, mock_memberships_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_memberships_list_two, 200 - ) - - syncify( - self.authorization.list_memberships_for_resource( - "authz_resource_01HXYZ", - permission_slug="document:read", - limit=25, - ) - ) - - assert request_kwargs["params"]["permission_slug"] == "document:read" - assert request_kwargs["params"]["limit"] == 25 - assert request_kwargs["params"]["order"] == "desc" - - def test_list_memberships_for_resource_with_before( - self, mock_memberships_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_memberships_list_two, 200 - ) - - syncify( - self.authorization.list_memberships_for_resource( - "authz_resource_01HXYZ", - permission_slug="document:read", - before="cursor_before", - ) - ) - - assert request_kwargs["params"]["before"] == "cursor_before" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - def test_list_memberships_for_resource_with_after( - self, mock_memberships_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_memberships_list_two, 200 - ) - - syncify( - self.authorization.list_memberships_for_resource( - "authz_resource_01HXYZ", - permission_slug="document:read", - after="cursor_after", - ) - ) - - assert request_kwargs["params"]["after"] == "cursor_after" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - def test_list_memberships_for_resource_with_order_asc( - self, mock_memberships_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_memberships_list_two, 200 - ) - - syncify( - self.authorization.list_memberships_for_resource( - "authz_resource_01HXYZ", - permission_slug="document:read", - order="asc", - ) - ) - - assert request_kwargs["params"]["order"] == "asc" - assert request_kwargs["params"]["limit"] == 10 - - def test_list_memberships_for_resource_with_order_desc( - self, mock_memberships_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_memberships_list_two, 200 - ) - - syncify( - self.authorization.list_memberships_for_resource( - "authz_resource_01HXYZ", - permission_slug="document:read", - order="desc", - ) - ) - - assert request_kwargs["params"]["order"] == "desc" - assert request_kwargs["params"]["limit"] == 10 - - def test_list_memberships_for_resource_with_all_parameters( - self, mock_memberships_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_memberships_list_two, 200 - ) - - response = syncify( - self.authorization.list_memberships_for_resource( - "authz_resource_01HXYZ", - permission_slug="document:read", - assignment="direct", - limit=5, - before="cursor_before", - after="cursor_after", - order="asc", - ) - ) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith( - "/authorization/resources/authz_resource_01HXYZ/organization_memberships" - ) - assert request_kwargs["params"]["permission_slug"] == "document:read" - assert request_kwargs["params"]["assignment"] == "direct" - assert request_kwargs["params"]["limit"] == 5 - assert request_kwargs["params"]["before"] == "cursor_before" - assert request_kwargs["params"]["after"] == "cursor_after" - assert request_kwargs["params"]["order"] == "asc" - - assert response.object == "list" - assert len(response.data) == 2 - - # --- list_memberships_for_resource_by_external_id --- - - def test_list_memberships_for_resource_by_external_id_returns_paginated_list( - self, mock_memberships_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_memberships_list_two, 200 - ) - - response = syncify( - self.authorization.list_memberships_for_resource_by_external_id( - organization_id="org_123", - resource_type_slug="document", - external_id="doc-ext-456", - permission_slug="document:read", - ) - ) - - assert request_kwargs["method"] == "get" - assert ( - "/authorization/organizations/org_123/resources/document/doc-ext-456/organization_memberships" - in request_kwargs["url"] - ) - assert request_kwargs["params"]["permission_slug"] == "document:read" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - assert response.object == "list" - assert len(response.data) == 2 - assert response.data[0].object == "organization_membership" - assert response.data[0].id == "om_01ABC" - assert response.data[0].user_id == "user_123" - assert response.data[0].organization_id == "org_456" - assert response.data[0].status == "active" - assert response.data[0].created_at == "2024-01-01T00:00:00Z" - assert response.data[0].updated_at == "2024-01-01T00:00:00Z" - assert response.data[1].object == "organization_membership" - assert response.data[1].id == "om_01DEF" - assert response.data[1].user_id == "user_789" - assert response.data[1].organization_id == "org_456" - assert response.data[1].status == "active" - assert response.data[1].created_at == "2024-01-02T00:00:00Z" - assert response.data[1].updated_at == "2024-01-02T00:00:00Z" - assert response.list_metadata.before is None - assert response.list_metadata.after == "om_01DEF" - - def test_list_memberships_for_resource_by_external_id_with_assignment_direct( - self, mock_memberships_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_memberships_list_two, 200 - ) - - syncify( - self.authorization.list_memberships_for_resource_by_external_id( - organization_id="org_123", - resource_type_slug="document", - external_id="doc-ext-456", - permission_slug="document:read", - assignment="direct", - ) - ) - - assert request_kwargs["params"]["permission_slug"] == "document:read" - assert request_kwargs["params"]["assignment"] == "direct" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - def test_list_memberships_for_resource_by_external_id_with_assignment_indirect( - self, mock_memberships_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_memberships_list_two, 200 - ) - - syncify( - self.authorization.list_memberships_for_resource_by_external_id( - organization_id="org_123", - resource_type_slug="folder", - external_id="folder-ext-789", - permission_slug="document:read", - assignment="indirect", - ) - ) - - assert request_kwargs["params"]["assignment"] == "indirect" - assert ( - "/authorization/organizations/org_123/resources/folder/folder-ext-789/organization_memberships" - in request_kwargs["url"] - ) - - def test_list_memberships_for_resource_by_external_id_with_limit( - self, mock_memberships_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_memberships_list_two, 200 - ) - - syncify( - self.authorization.list_memberships_for_resource_by_external_id( - organization_id="org_123", - resource_type_slug="document", - external_id="doc-ext-456", - permission_slug="document:read", - limit=25, - ) - ) - - assert request_kwargs["params"]["permission_slug"] == "document:read" - assert request_kwargs["params"]["limit"] == 25 - assert request_kwargs["params"]["order"] == "desc" - - def test_list_memberships_for_resource_by_external_id_with_before( - self, mock_memberships_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_memberships_list_two, 200 - ) - - syncify( - self.authorization.list_memberships_for_resource_by_external_id( - organization_id="org_123", - resource_type_slug="document", - external_id="doc-ext-456", - permission_slug="document:read", - before="cursor_before", - ) - ) - - assert request_kwargs["params"]["before"] == "cursor_before" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - def test_list_memberships_for_resource_by_external_id_with_after( - self, mock_memberships_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_memberships_list_two, 200 - ) - - syncify( - self.authorization.list_memberships_for_resource_by_external_id( - organization_id="org_123", - resource_type_slug="document", - external_id="doc-ext-456", - permission_slug="document:read", - after="cursor_after", - ) - ) - - assert request_kwargs["params"]["after"] == "cursor_after" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - - def test_list_memberships_for_resource_by_external_id_with_order_asc( - self, mock_memberships_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_memberships_list_two, 200 - ) - - syncify( - self.authorization.list_memberships_for_resource_by_external_id( - organization_id="org_123", - resource_type_slug="document", - external_id="doc-ext-456", - permission_slug="document:read", - order="asc", - ) - ) - - assert request_kwargs["params"]["order"] == "asc" - assert request_kwargs["params"]["limit"] == 10 - - def test_list_memberships_for_resource_by_external_id_with_order_desc( - self, mock_memberships_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_memberships_list_two, 200 - ) - - syncify( - self.authorization.list_memberships_for_resource_by_external_id( - organization_id="org_123", - resource_type_slug="document", - external_id="doc-ext-456", - permission_slug="document:read", - order="desc", - ) - ) - - assert request_kwargs["params"]["order"] == "desc" - assert request_kwargs["params"]["limit"] == 10 - - def test_list_memberships_for_resource_by_external_id_with_all_parameters( - self, mock_memberships_list_two, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_memberships_list_two, 200 - ) - - response = syncify( - self.authorization.list_memberships_for_resource_by_external_id( - organization_id="org_123", - resource_type_slug="document", - external_id="doc-ext-456", - permission_slug="document:read", - assignment="direct", - limit=5, - before="cursor_before", - after="cursor_after", - order="asc", - ) - ) - - assert request_kwargs["method"] == "get" - assert ( - "/authorization/organizations/org_123/resources/document/doc-ext-456/organization_memberships" - in request_kwargs["url"] - ) - assert request_kwargs["params"]["permission_slug"] == "document:read" - assert request_kwargs["params"]["assignment"] == "direct" - assert request_kwargs["params"]["limit"] == 5 - assert request_kwargs["params"]["before"] == "cursor_before" - assert request_kwargs["params"]["after"] == "cursor_after" - assert request_kwargs["params"]["order"] == "asc" - - assert response.object == "list" - assert len(response.data) == 2 diff --git a/tests/test_authorization_role_assignments.py b/tests/test_authorization_role_assignments.py deleted file mode 100644 index ed040ece..00000000 --- a/tests/test_authorization_role_assignments.py +++ /dev/null @@ -1,358 +0,0 @@ -from typing import Union - -import pytest -from tests.utils.fixtures.mock_role_assignment import ( - MockRoleAssignment, - MockRoleAssignmentsList, -) -from tests.utils.list_resource import list_response_of -from tests.utils.syncify import syncify -from workos.authorization import AsyncAuthorization, Authorization - - -@pytest.mark.sync_and_async(Authorization, AsyncAuthorization) -class TestAuthorizationRoleAssignments: - @pytest.fixture(autouse=True) - def setup(self, module_instance: Union[Authorization, AsyncAuthorization]): - self.http_client = module_instance._http_client - self.authorization = module_instance - - @pytest.fixture - def mock_role_assignments_list(self): - return MockRoleAssignmentsList().dict() - - @pytest.fixture - def mock_role_assignments_empty_list(self): - return list_response_of(data=[]) - - def test_assign_role_by_resource_id(self, capture_and_mock_http_client_request): - mock_role_assignment = MockRoleAssignment().dict() - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_role_assignment, 201 - ) - - response = syncify( - self.authorization.assign_role( - "om_01ABC", - role_slug="admin", - resource_identifier={"resource_id": "res_01XYZ"}, - ) - ) - - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith( - "/authorization/organization_memberships/om_01ABC/role_assignments" - ) - assert request_kwargs["json"] == { - "role_slug": "admin", - "resource_id": "res_01XYZ", - } - assert "resource_external_id" not in request_kwargs["json"] - assert "resource_type_slug" not in request_kwargs["json"] - - assert response.dict() == mock_role_assignment - - def test_assign_role_by_external_id_and_resource_type_slug( - self, capture_and_mock_http_client_request - ): - mock_role_assignment = MockRoleAssignment().dict() - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_role_assignment, 201 - ) - - response = syncify( - self.authorization.assign_role( - "om_01ABC", - role_slug="editor", - resource_identifier={ - "resource_external_id": "ext_doc_456", - "resource_type_slug": "document", - }, - ) - ) - - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith( - "/authorization/organization_memberships/om_01ABC/role_assignments" - ) - assert request_kwargs["json"] == { - "role_slug": "editor", - "resource_external_id": "ext_doc_456", - "resource_type_slug": "document", - } - assert "resource_id" not in request_kwargs["json"] - - assert response.dict() == mock_role_assignment - - def test_remove_role_by_resource_id(self, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, status_code=204 - ) - - syncify( - self.authorization.remove_role( - "om_01ABC", - role_slug="admin", - resource_identifier={"resource_id": "res_01XYZ"}, - ) - ) - - assert request_kwargs["method"] == "delete" - assert request_kwargs["url"].endswith( - "/authorization/organization_memberships/om_01ABC/role_assignments" - ) - assert request_kwargs["json"] == { - "role_slug": "admin", - "resource_id": "res_01XYZ", - } - assert "resource_external_id" not in request_kwargs["json"] - assert "resource_type_slug" not in request_kwargs["json"] - - def test_remove_role_by_external_id_and_resource_type_slug( - self, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, status_code=204 - ) - - syncify( - self.authorization.remove_role( - "om_01ABC", - role_slug="editor", - resource_identifier={ - "resource_external_id": "ext_doc_456", - "resource_type_slug": "document", - }, - ) - ) - - assert request_kwargs["method"] == "delete" - assert request_kwargs["url"].endswith( - "/authorization/organization_memberships/om_01ABC/role_assignments" - ) - assert request_kwargs["json"] == { - "role_slug": "editor", - "resource_external_id": "ext_doc_456", - "resource_type_slug": "document", - } - assert "resource_id" not in request_kwargs["json"] - - def test_remove_role_assignment(self, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, status_code=204 - ) - - syncify( - self.authorization.remove_role_assignment( - "om_01ABC", - role_assignment_id="ra_01XYZ", - ) - ) - - assert request_kwargs["method"] == "delete" - assert request_kwargs["url"].endswith( - "/authorization/organization_memberships/om_01ABC/role_assignments/ra_01XYZ" - ) - - def test_list_role_assignments_returns_paginated_list( - self, - mock_role_assignments_list, - capture_and_mock_http_client_request, - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_role_assignments_list, 200 - ) - - response = syncify( - self.authorization.list_role_assignments( - organization_membership_id="om_01ABC" - ) - ) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith( - "/authorization/organization_memberships/om_01ABC/role_assignments" - ) - assert request_kwargs["params"] == {"limit": 10, "order": "desc"} - - assert response.object == "list" - assert len(response.data) == 2 - - assert response.data[0].object == "role_assignment" - assert response.data[0].id == "ra_01ABC" - assert response.data[0].role.slug == "admin" - assert response.data[0].resource.id == "res_01ABC" - assert response.data[0].resource.external_id == "ext_123" - assert response.data[0].resource.resource_type_slug == "document" - assert response.data[0].created_at == "2024-01-15T09:30:00.000Z" - assert response.data[0].updated_at == "2024-01-15T09:30:00.000Z" - - assert response.data[1].object == "role_assignment" - assert response.data[1].id == "ra_01DEF" - assert response.data[1].role.slug == "editor" - assert response.data[1].resource.id == "res_01XYZ" - assert response.data[1].resource.external_id == "ext_456" - assert response.data[1].resource.resource_type_slug == "folder" - assert response.data[1].created_at == "2024-01-14T08:00:00.000Z" - assert response.data[1].updated_at == "2024-01-14T08:00:00.000Z" - - assert response.list_metadata.before is None - assert response.list_metadata.after == "ra_01DEF" - - def test_list_role_assignments_returns_empty_list( - self, - mock_role_assignments_empty_list, - capture_and_mock_http_client_request, - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_role_assignments_empty_list, 200 - ) - - response = syncify( - self.authorization.list_role_assignments( - organization_membership_id="om_01ABC" - ) - ) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith( - "/authorization/organization_memberships/om_01ABC/role_assignments" - ) - assert request_kwargs["params"] == {"limit": 10, "order": "desc"} - - assert len(response.data) == 0 - assert response.list_metadata.before is None - assert response.list_metadata.after is None - - def test_list_role_assignments_with_limit( - self, - mock_role_assignments_list, - capture_and_mock_http_client_request, - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_role_assignments_list, 200 - ) - - syncify( - self.authorization.list_role_assignments( - organization_membership_id="om_01ABC", - limit=25, - ) - ) - - assert request_kwargs["params"]["limit"] == 25 - assert request_kwargs["params"]["order"] == "desc" - assert "before" not in request_kwargs["params"] - assert "after" not in request_kwargs["params"] - - def test_list_role_assignments_with_before( - self, - mock_role_assignments_list, - capture_and_mock_http_client_request, - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_role_assignments_list, 200 - ) - - syncify( - self.authorization.list_role_assignments( - organization_membership_id="om_01ABC", - before="cursor_before", - ) - ) - - assert request_kwargs["params"]["before"] == "cursor_before" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - assert "after" not in request_kwargs["params"] - - def test_list_role_assignments_with_after( - self, - mock_role_assignments_list, - capture_and_mock_http_client_request, - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_role_assignments_list, 200 - ) - - syncify( - self.authorization.list_role_assignments( - organization_membership_id="om_01ABC", - after="cursor_after", - ) - ) - - assert request_kwargs["params"]["after"] == "cursor_after" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["order"] == "desc" - assert "before" not in request_kwargs["params"] - - def test_list_role_assignments_with_order_desc( - self, - mock_role_assignments_list, - capture_and_mock_http_client_request, - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_role_assignments_list, 200 - ) - - syncify( - self.authorization.list_role_assignments( - organization_membership_id="om_01ABC", - order="desc", - ) - ) - - assert request_kwargs["params"]["order"] == "desc" - assert request_kwargs["params"]["limit"] == 10 - assert "before" not in request_kwargs["params"] - assert "after" not in request_kwargs["params"] - - def test_list_role_assignments_with_order_asc( - self, - mock_role_assignments_list, - capture_and_mock_http_client_request, - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_role_assignments_list, 200 - ) - - syncify( - self.authorization.list_role_assignments( - organization_membership_id="om_01ABC", - order="asc", - ) - ) - - assert request_kwargs["params"]["order"] == "asc" - assert request_kwargs["params"]["limit"] == 10 - assert "before" not in request_kwargs["params"] - assert "after" not in request_kwargs["params"] - - def test_list_role_assignments_with_all_parameters( - self, - mock_role_assignments_list, - capture_and_mock_http_client_request, - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_role_assignments_list, 200 - ) - - syncify( - self.authorization.list_role_assignments( - organization_membership_id="om_01ABC", - limit=5, - before="cursor_before", - after="cursor_after", - order="asc", - ) - ) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith( - "/authorization/organization_memberships/om_01ABC/role_assignments" - ) - assert request_kwargs["params"]["limit"] == 5 - assert request_kwargs["params"]["before"] == "cursor_before" - assert request_kwargs["params"]["after"] == "cursor_after" - assert request_kwargs["params"]["order"] == "asc" diff --git a/tests/test_client.py b/tests/test_client.py deleted file mode 100644 index 9a2c1a81..00000000 --- a/tests/test_client.py +++ /dev/null @@ -1,128 +0,0 @@ -import os -import pytest -from workos import AsyncWorkOSClient, WorkOSClient - - -class TestClient: - @pytest.fixture - def default_client(self): - return WorkOSClient( - api_key="sk_test", client_id="client_b27needthisforssotemxo" - ) - - def test_client_without_api_key(self): - with pytest.raises(ValueError) as error: - WorkOSClient(client_id="client_b27needthisforssotemxo") - - assert ( - "WorkOS API key must be provided when instantiating the client or via the WORKOS_API_KEY environment variable." - == str(error.value) - ) - - def test_client_without_client_id(self): - with pytest.raises(ValueError) as error: - WorkOSClient(api_key="sk_test") - - assert ( - "WorkOS client ID must be provided when instantiating the client or via the WORKOS_CLIENT_ID environment variable." - == str(error.value) - ) - - def test_client_with_api_key_and_client_id_environment_variables(self): - os.environ["WORKOS_API_KEY"] = "sk_test" - os.environ["WORKOS_CLIENT_ID"] = "client_b27needthisforssotemxo" - - assert bool(WorkOSClient()) - - os.environ.pop("WORKOS_API_KEY") - os.environ.pop("WORKOS_CLIENT_ID") - - def test_initialize_api_keys(self, default_client): - assert bool(default_client.api_keys) - - def test_initialize_sso(self, default_client): - assert bool(default_client.sso) - - def test_initialize_audit_logs(self, default_client): - assert bool(default_client.audit_logs) - - def test_initialize_directory_sync(self, default_client): - assert bool(default_client.directory_sync) - - def test_initialize_events(self, default_client): - assert bool(default_client.events) - - def test_initialize_mfa(self, default_client): - assert bool(default_client.mfa) - - def test_initialize_organizations(self, default_client): - assert bool(default_client.organizations) - - def test_initialize_passwordless(self, default_client): - assert bool(default_client.passwordless) - - def test_initialize_portal(self, default_client): - assert bool(default_client.portal) - - def test_initialize_user_management(self, default_client): - assert bool(default_client.user_management) - - def test_initialize_widgets(self, default_client): - assert bool(default_client.widgets) - - def test_enforce_trailing_slash_for_base_url( - self, - ): - client = WorkOSClient( - api_key="sk_test", - client_id="client_b27needthisforssotemxo", - base_url="https://api.workos.com", - ) - assert client.base_url == "https://api.workos.com/" - - -class TestAsyncClient: - @pytest.fixture - def default_client(self): - return AsyncWorkOSClient( - api_key="sk_test", client_id="client_b27needthisforssotemxo" - ) - - def test_client_without_api_key(self): - with pytest.raises(ValueError) as error: - AsyncWorkOSClient(client_id="client_b27needthisforssotemxo") - - assert ( - "WorkOS API key must be provided when instantiating the client or via the WORKOS_API_KEY environment variable." - == str(error.value) - ) - - def test_client_without_client_id(self): - with pytest.raises(ValueError) as error: - AsyncWorkOSClient(api_key="sk_test") - - assert ( - "WorkOS client ID must be provided when instantiating the client or via the WORKOS_CLIENT_ID environment variable." - == str(error.value) - ) - - def test_client_with_api_key_and_client_id_environment_variables(self): - os.environ["WORKOS_API_KEY"] = "sk_test" - os.environ["WORKOS_CLIENT_ID"] = "client_b27needthisforssotemxo" - - assert bool(AsyncWorkOSClient()) - - os.environ.pop("WORKOS_API_KEY") - os.environ.pop("WORKOS_CLIENT_ID") - - def test_initialize_api_keys(self, default_client): - assert bool(default_client.api_keys) - - def test_initialize_directory_sync(self, default_client): - assert bool(default_client.directory_sync) - - def test_initialize_events(self, default_client): - assert bool(default_client.events) - - def test_initialize_sso(self, default_client): - assert bool(default_client.sso) diff --git a/tests/test_connect.py b/tests/test_connect.py deleted file mode 100644 index 1c0823fb..00000000 --- a/tests/test_connect.py +++ /dev/null @@ -1,261 +0,0 @@ -from typing import Union -import pytest -from tests.types.test_auto_pagination_function import TestAutoPaginationFunction -from tests.utils.fixtures.mock_client_secret import MockClientSecret -from tests.utils.fixtures.mock_connect_application import MockConnectApplication -from tests.utils.list_resource import list_response_of -from tests.utils.syncify import syncify -from workos.connect import AsyncConnect, Connect - - -@pytest.mark.sync_and_async(Connect, AsyncConnect) -class TestConnect: - @pytest.fixture(autouse=True) - def setup(self, module_instance: Union[Connect, AsyncConnect]): - self.http_client = module_instance._http_client - self.connect = module_instance - - @pytest.fixture - def mock_application(self): - return MockConnectApplication("app_01ABC").dict() - - @pytest.fixture - def mock_oauth_application(self): - return MockConnectApplication("app_01ABC", application_type="oauth").dict() - - @pytest.fixture - def mock_applications(self): - application_list = [MockConnectApplication(id=str(i)).dict() for i in range(10)] - return { - "data": application_list, - "list_metadata": {"before": None, "after": None}, - "object": "list", - } - - @pytest.fixture - def mock_applications_multiple_data_pages(self): - applications_list = [ - MockConnectApplication(id=f"app_{i + 1}").dict() for i in range(40) - ] - return list_response_of(data=applications_list) - - @pytest.fixture - def mock_client_secret(self): - return MockClientSecret("cs_01ABC", include_secret=True).dict() - - @pytest.fixture - def mock_client_secrets(self): - return [MockClientSecret(id=f"cs_{i}").dict() for i in range(10)] - - # --- Application Tests --- - - def test_list_applications( - self, mock_applications, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_applications, 200 - ) - - response = syncify(self.connect.list_applications()) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/connect/applications") - assert list(map(lambda x: x.dict(), response.data)) == mock_applications["data"] - - def test_list_applications_with_organization_id( - self, mock_applications, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_applications, 200 - ) - - syncify(self.connect.list_applications(organization_id="org_01ABC")) - - assert request_kwargs["method"] == "get" - assert request_kwargs["params"]["organization_id"] == "org_01ABC" - - def test_get_application( - self, mock_application, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_application, 200 - ) - - application = syncify(self.connect.get_application(application_id="app_01ABC")) - - assert application.dict() == mock_application - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/connect/applications/app_01ABC") - - def test_get_oauth_application( - self, mock_oauth_application, capture_and_mock_http_client_request - ): - capture_and_mock_http_client_request( - self.http_client, mock_oauth_application, 200 - ) - - application = syncify(self.connect.get_application(application_id="app_01ABC")) - - assert application.dict() == mock_oauth_application - assert application.application_type == "oauth" - assert application.redirect_uris is not None - assert application.uses_pkce is True - assert application.is_first_party is True - - def test_create_m2m_application( - self, mock_application, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_application, 201 - ) - - application = syncify( - self.connect.create_application( - name="Test Application", - application_type="m2m", - is_first_party=True, - organization_id="org_01ABC", - scopes=["read", "write"], - ) - ) - - assert application.id == "app_01ABC" - assert application.name == "Test Application" - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith("/connect/applications") - assert request_kwargs["json"]["name"] == "Test Application" - assert request_kwargs["json"]["application_type"] == "m2m" - assert request_kwargs["json"]["organization_id"] == "org_01ABC" - - def test_create_oauth_application( - self, mock_oauth_application, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_oauth_application, 201 - ) - - application = syncify( - self.connect.create_application( - name="Test Application", - application_type="oauth", - is_first_party=True, - redirect_uris=[ - {"uri": "https://example.com/callback", "default": True} - ], - uses_pkce=True, - ) - ) - - assert application.application_type == "oauth" - assert request_kwargs["method"] == "post" - assert request_kwargs["json"]["application_type"] == "oauth" - assert request_kwargs["json"]["redirect_uris"] == [ - {"uri": "https://example.com/callback", "default": True} - ] - - def test_update_application( - self, mock_application, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_application, 200 - ) - - syncify( - self.connect.update_application( - application_id="app_01ABC", - name="Updated Name", - scopes=["read"], - ) - ) - - assert request_kwargs["method"] == "put" - assert request_kwargs["url"].endswith("/connect/applications/app_01ABC") - assert request_kwargs["json"]["name"] == "Updated Name" - assert request_kwargs["json"]["scopes"] == ["read"] - - def test_delete_application(self, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, - None, - 202, - headers={"content-type": "text/plain; charset=utf-8"}, - ) - - response = syncify(self.connect.delete_application(application_id="app_01ABC")) - - assert request_kwargs["url"].endswith("/connect/applications/app_01ABC") - assert request_kwargs["method"] == "delete" - assert response is None - - def test_list_applications_auto_pagination_for_single_page( - self, - mock_applications, - test_auto_pagination: TestAutoPaginationFunction, - ): - test_auto_pagination( - http_client=self.http_client, - list_function=self.connect.list_applications, - expected_all_page_data=mock_applications["data"], - ) - - def test_list_applications_auto_pagination_for_multiple_pages( - self, - mock_applications_multiple_data_pages, - test_auto_pagination: TestAutoPaginationFunction, - ): - test_auto_pagination( - http_client=self.http_client, - list_function=self.connect.list_applications, - expected_all_page_data=mock_applications_multiple_data_pages["data"], - ) - - # --- Client Secret Tests --- - - def test_create_client_secret( - self, mock_client_secret, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_client_secret, 201 - ) - - secret = syncify(self.connect.create_client_secret(application_id="app_01ABC")) - - assert secret.id == "cs_01ABC" - assert secret.secret == "sk_test_secret_value_123" - assert secret.secret_hint == "...abcd" - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith( - "/connect/applications/app_01ABC/client_secrets" - ) - assert request_kwargs["json"] == {} - - def test_list_client_secrets( - self, mock_client_secrets, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_client_secrets, 200 - ) - - response = syncify(self.connect.list_client_secrets(application_id="app_01ABC")) - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith( - "/connect/applications/app_01ABC/client_secrets" - ) - assert [secret.dict() for secret in response] == mock_client_secrets - - def test_delete_client_secret(self, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, - None, - 202, - headers={"content-type": "text/plain; charset=utf-8"}, - ) - - response = syncify( - self.connect.delete_client_secret(client_secret_id="cs_01ABC") - ) - - assert request_kwargs["url"].endswith("/connect/client_secrets/cs_01ABC") - assert request_kwargs["method"] == "delete" - assert response is None diff --git a/tests/test_directory_sync.py b/tests/test_directory_sync.py deleted file mode 100644 index b9be86ef..00000000 --- a/tests/test_directory_sync.py +++ /dev/null @@ -1,431 +0,0 @@ -from typing import Union - -import pytest - -from tests.types.test_auto_pagination_function import TestAutoPaginationFunction -from tests.utils.fixtures.mock_directory import ( - MockDirectory, - MockDirectoryMetadata, - MockDirectoryUsersMetadata, -) -from tests.utils.fixtures.mock_directory_group import MockDirectoryGroup -from tests.utils.fixtures.mock_directory_user import MockDirectoryUser -from tests.utils.list_resource import list_data_to_dicts, list_response_of -from tests.utils.syncify import syncify -from workos.directory_sync import ( - _prepare_request_params, - AsyncDirectorySync, - DirectorySync, -) -from workos.types.directory_sync.list_filters import ( - DirectoryGroupListFilters, - DirectoryUserListFilters, -) - - -def api_directory_to_sdk(directory): - # The API returns an active directory as 'linked' - # We normalize this to 'active' in the SDK. This helper function - # does this conversion to make make assertions easier. - if directory["state"] == "linked": - return {**directory, "state": "active"} - else: - return directory - - -def api_directories_to_sdk(directories): - return list(map(lambda x: api_directory_to_sdk(x), directories)) - - -@pytest.mark.sync_and_async(DirectorySync, AsyncDirectorySync) -class TestDirectorySync: - @pytest.fixture(autouse=True) - def setup(self, module_instance: Union[DirectorySync, AsyncDirectorySync]): - self.http_client = module_instance._http_client - self.directory_sync = module_instance - - @pytest.fixture - def mock_users(self): - user_list = [MockDirectoryUser(id=str(i)).dict() for i in range(100)] - - return { - "data": user_list, - "list_metadata": {"before": None, "after": None}, - "object": "list", - } - - @pytest.fixture - def mock_groups(self): - group_list = [MockDirectoryGroup(id=str(i)).dict() for i in range(20)] - return list_response_of(data=group_list, after="xxx") - - @pytest.fixture - def mock_user_primary_email(self): - return {"primary": True, "type": "work", "value": "marcelina@foo-corp.com"} - - @pytest.fixture - def mock_user(self): - return MockDirectoryUser("directory_user_01E1JG7J09H96KYP8HM9B0G5SJ").dict() - - @pytest.fixture - def mock_user_no_email(self): - return { - "id": "directory_user_01E1JG7J09H96KYP8HM9B0GZZZ", - "object": "directory_user", - "idp_id": "2836", - "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", - "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", - "first_name": "Marcelina", - "last_name": "Davis", - "email": None, - "job_title": "Software Engineer", - "emails": [], - "username": "marcelina@foo-corp.com", - "groups": [ - { - "id": "directory_group_01E64QTDNS0EGJ0FMCVY9BWGZT", - "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", - "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", - "object": "directory_group", - "idp_id": "2836", - "name": "Engineering", - "created_at": "2021-06-25T19:07:33.155Z", - "updated_at": "2021-06-25T19:07:33.155Z", - "raw_attributes": {"work_email": "124@gmail.com"}, - } - ], - "state": "active", - "created_at": "2021-06-25T19:07:33.155Z", - "updated_at": "2021-06-25T19:07:33.155Z", - "custom_attributes": {"department": "Engineering"}, - "raw_attributes": {}, - } - - @pytest.fixture - def mock_group(self): - return MockDirectoryGroup("directory_group_01FHGRYAQ6ERZXXXXXX1E01QFE").dict() - - @pytest.fixture - def mock_directories(self): - directory_list = [MockDirectory(id=str(i)).dict() for i in range(10)] - return list_response_of(data=directory_list) - - @pytest.fixture - def mock_directories_with_metadata(self): - metadata = MockDirectoryMetadata( - users=MockDirectoryUsersMetadata(active=1, inactive=0), groups=1 - ) - directory_list = [ - MockDirectory(id=str(i), metadata=metadata).dict() for i in range(10) - ] - return list_response_of(data=directory_list) - - @pytest.fixture - def mock_directory_users_multiple_data_pages(self): - return [ - MockDirectoryUser(id=str(f"directory_user_{i}")).dict() for i in range(40) - ] - - @pytest.fixture - def mock_directories_multiple_data_pages(self): - return [MockDirectory(id=str(f"dir_{i}")).dict() for i in range(40)] - - @pytest.fixture - def mock_directory_groups_multiple_data_pages(self): - return [ - MockDirectoryGroup(id=str(f"directory_group_{i}")).dict() for i in range(40) - ] - - @pytest.fixture - def mock_directory(self): - return MockDirectory("directory_id").dict() - - def test_list_users_with_directory( - self, mock_users, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - http_client=self.http_client, status_code=200, response_dict=mock_users - ) - - users = syncify(self.directory_sync.list_users(directory_id="directory_id")) - - assert list_data_to_dicts(users.data) == mock_users["data"] - assert request_kwargs["url"].endswith("/directory_users") - assert request_kwargs["method"] == "get" - assert request_kwargs["params"] == { - "directory": "directory_id", - "limit": 10, - "order": "desc", - } - - def test_list_users_with_group( - self, mock_users, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - http_client=self.http_client, status_code=200, response_dict=mock_users - ) - - users = syncify(self.directory_sync.list_users(group_id="directory_grp_id")) - - assert list_data_to_dicts(users.data) == mock_users["data"] - assert request_kwargs["url"].endswith("/directory_users") - assert request_kwargs["method"] == "get" - assert request_kwargs["params"] == { - "group": "directory_grp_id", - "limit": 10, - "order": "desc", - } - - def test_list_groups_with_directory( - self, mock_groups, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - http_client=self.http_client, status_code=200, response_dict=mock_groups - ) - - groups = syncify(self.directory_sync.list_groups(directory_id="directory_id")) - - assert list_data_to_dicts(groups.data) == mock_groups["data"] - assert request_kwargs["url"].endswith("/directory_groups") - assert request_kwargs["method"] == "get" - assert request_kwargs["params"] == { - "directory": "directory_id", - "limit": 10, - "order": "desc", - } - - def test_list_groups_with_user( - self, mock_groups, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - http_client=self.http_client, status_code=200, response_dict=mock_groups - ) - - groups = syncify(self.directory_sync.list_groups(user_id="directory_user_id")) - - assert list_data_to_dicts(groups.data) == mock_groups["data"] - assert request_kwargs["url"].endswith("/directory_groups") - assert request_kwargs["method"] == "get" - assert request_kwargs["params"] == { - "user": "directory_user_id", - "limit": 10, - "order": "desc", - } - - def test_get_user(self, mock_user, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - http_client=self.http_client, - status_code=200, - response_dict=mock_user, - ) - - user = syncify(self.directory_sync.get_user(user_id="directory_user_id")) - - assert user.dict() == mock_user - assert request_kwargs["url"].endswith("/directory_users/directory_user_id") - assert request_kwargs["method"] == "get" - - def test_get_group(self, mock_group, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - http_client=self.http_client, - status_code=200, - response_dict=mock_group, - ) - - group = syncify( - self.directory_sync.get_group( - group_id="directory_group_01FHGRYAQ6ERZXXXXXX1E01QFE" - ) - ) - - assert group.dict() == mock_group - assert request_kwargs["url"].endswith( - "/directory_groups/directory_group_01FHGRYAQ6ERZXXXXXX1E01QFE" - ) - assert request_kwargs["method"] == "get" - - def test_list_directories( - self, mock_directories, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - http_client=self.http_client, - status_code=200, - response_dict=mock_directories, - ) - - directories = syncify(self.directory_sync.list_directories()) - - assert list_data_to_dicts(directories.data) == api_directories_to_sdk( - mock_directories["data"] - ) - assert request_kwargs["url"].endswith("/directories") - assert request_kwargs["method"] == "get" - assert request_kwargs["params"] == { - "limit": 10, - "order": "desc", - } - - def test_list_directories_with_metadata( - self, mock_directories_with_metadata, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - http_client=self.http_client, - status_code=200, - response_dict=mock_directories_with_metadata, - ) - - directories = syncify(self.directory_sync.list_directories()) - - assert list_data_to_dicts(directories.data) == api_directories_to_sdk( - mock_directories_with_metadata["data"] - ) - assert request_kwargs["url"].endswith("/directories") - assert request_kwargs["method"] == "get" - assert request_kwargs["params"] == { - "limit": 10, - "order": "desc", - } - - def test_get_directory(self, mock_directory, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - http_client=self.http_client, - status_code=200, - response_dict=mock_directory, - ) - - directory = syncify( - self.directory_sync.get_directory(directory_id="directory_id") - ) - - assert directory.dict() == api_directory_to_sdk(mock_directory) - assert request_kwargs["url"].endswith("/directories/directory_id") - assert request_kwargs["method"] == "get" - - def test_delete_directory(self, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - http_client=self.http_client, - status_code=202, - headers={"content-type": "text/plain; charset=utf-8"}, - ) - - response = syncify( - self.directory_sync.delete_directory(directory_id="directory_id") - ) - - assert request_kwargs["url"].endswith("/directories/directory_id") - assert request_kwargs["method"] == "delete" - assert response is None - - def test_primary_email( - self, mock_user, mock_user_primary_email, mock_http_client_with_response - ): - mock_http_client_with_response( - http_client=self.http_client, - status_code=200, - response_dict=mock_user, - ) - mock_user_instance = syncify( - self.directory_sync.get_user("directory_user_01E1JG7J09H96KYP8HM9B0G5SJ") - ) - primary_email = mock_user_instance.primary_email() - assert primary_email - assert primary_email.dict() == mock_user_primary_email - - def test_primary_email_none( - self, mock_user_no_email, mock_http_client_with_response - ): - mock_http_client_with_response( - http_client=self.http_client, - status_code=200, - response_dict=mock_user_no_email, - ) - mock_user_instance = syncify( - self.directory_sync.get_user("directory_user_01E1JG7J09H96KYP8HM9B0G5SJ") - ) - - me = mock_user_instance.primary_email() - - assert me is None - - def test_list_directories_auto_pagination( - self, - mock_directories_multiple_data_pages, - test_auto_pagination: TestAutoPaginationFunction, - ): - test_auto_pagination( - http_client=self.http_client, - list_function=self.directory_sync.list_directories, - expected_all_page_data=mock_directories_multiple_data_pages, - ) - - def test_directory_users_auto_pagination( - self, - mock_directory_users_multiple_data_pages, - test_auto_pagination: TestAutoPaginationFunction, - ): - test_auto_pagination( - http_client=self.http_client, - list_function=self.directory_sync.list_users, - expected_all_page_data=mock_directory_users_multiple_data_pages, - ) - - def test_directory_user_groups_auto_pagination( - self, - mock_directory_groups_multiple_data_pages, - test_auto_pagination: TestAutoPaginationFunction, - ): - test_auto_pagination( - http_client=self.http_client, - list_function=self.directory_sync.list_groups, - expected_all_page_data=mock_directory_groups_multiple_data_pages, - ) - - -class TestPrepareRequestParams: - """Tests for SDK-to-API parameter name translation. - - The SDK uses Pythonic parameter names (directory_id, group_id, user_id) - but the WorkOS API expects shorter names (directory, group, user). - The _prepare_request_params function handles this translation. - - See: https://github.com/workos/workos-python/issues/511 - See: https://github.com/workos/workos-python/issues/519 - """ - - def test_translates_directory_id_to_directory(self): - params: DirectoryUserListFilters = {"directory_id": "dir_123", "limit": 10} - result = _prepare_request_params(params) - assert "directory" in result - assert "directory_id" not in result - assert result["directory"] == "dir_123" - - def test_translates_group_id_to_group(self): - params: DirectoryUserListFilters = {"group_id": "grp_123", "limit": 10} - result = _prepare_request_params(params) - assert "group" in result - assert "group_id" not in result - assert result["group"] == "grp_123" - - def test_translates_user_id_to_user(self): - params: DirectoryGroupListFilters = {"user_id": "usr_123", "limit": 10} - result = _prepare_request_params(params) - assert "user" in result - assert "user_id" not in result - assert result["user"] == "usr_123" - - def test_preserves_non_id_params(self): - params: DirectoryUserListFilters = { - "directory_id": "dir_123", - "limit": 10, - "order": "desc", - "after": "cursor", - } - result = _prepare_request_params(params) - assert result["limit"] == 10 - assert result["order"] == "desc" - assert result["after"] == "cursor" - - def test_handles_empty_params(self): - params: DirectoryUserListFilters = {"limit": 10, "order": "desc"} - result = _prepare_request_params(params) - assert result == {"limit": 10, "order": "desc"} diff --git a/tests/test_events.py b/tests/test_events.py deleted file mode 100644 index 799fc34c..00000000 --- a/tests/test_events.py +++ /dev/null @@ -1,514 +0,0 @@ -from typing import Union - -import pytest - -from tests.utils.fixtures.mock_event import MockEvent -from tests.utils.syncify import syncify -from workos.events import AsyncEvents, Events, EventsListResource -from workos.types.events import OrganizationMembershipCreatedEvent -from workos.types.events.event import ( - VaultDataCreatedEvent, - VaultDataDeletedEvent, - VaultDataReadEvent, - VaultDataUpdatedEvent, - VaultDekDecryptedEvent, - VaultDekReadEvent, - VaultKekCreatedEvent, - VaultMetadataReadEvent, - VaultNamesListedEvent, -) - - -@pytest.mark.sync_and_async(Events, AsyncEvents) -class TestEvents(object): - @pytest.fixture - def mock_events(self): - events = [MockEvent(id=str(i)).dict() for i in range(10)] - - return { - "object": "list", - "data": events, - "list_metadata": { - "after": None, - }, - } - - def test_list_events( - self, - module_instance: Union[Events, AsyncEvents], - mock_events: EventsListResource, - capture_and_mock_http_client_request, - ): - request_kwargs = capture_and_mock_http_client_request( - http_client=module_instance._http_client, - status_code=200, - response_dict=mock_events, - ) - - events: EventsListResource = syncify( - module_instance.list_events(events=["dsync.activated"]) - ) - - assert request_kwargs["url"].endswith("/events") - assert request_kwargs["method"] == "get" - assert request_kwargs["params"] == {"events": ["dsync.activated"], "limit": 10} - assert events.dict() == mock_events - - def test_list_events_organization_membership_missing_custom_attributes( - self, - module_instance: Union[Events, AsyncEvents], - capture_and_mock_http_client_request, - ): - mock_response = { - "object": "list", - "data": [ - { - "object": "event", - "id": "event_01234", - "event": "organization_membership.created", - "data": { - "object": "organization_membership", - "id": "om_01234", - "user_id": "user_01234", - "organization_id": "org_01234", - "organization_name": "Foo Corp", - "role": {"slug": "member"}, - "status": "active", - "created_at": "2024-01-01T00:00:00.000Z", - "updated_at": "2024-01-01T00:00:00.000Z", - }, - "created_at": "2024-01-01T00:00:00.000Z", - } - ], - "list_metadata": { - "after": None, - }, - } - - capture_and_mock_http_client_request( - http_client=module_instance._http_client, - status_code=200, - response_dict=mock_response, - ) - - events: EventsListResource = syncify( - module_instance.list_events(events=["organization_membership.created"]) - ) - - event = events.data[0] - assert isinstance(event, OrganizationMembershipCreatedEvent) - assert event.data.custom_attributes == {} - - def test_list_events_organization_membership_missing_organization_name( - self, - module_instance: Union[Events, AsyncEvents], - capture_and_mock_http_client_request, - ): - mock_response = { - "object": "list", - "data": [ - { - "object": "event", - "id": "event_01234", - "event": "organization_membership.created", - "data": { - "object": "organization_membership", - "id": "om_01234", - "user_id": "user_01234", - "organization_id": "org_01234", - "role": {"slug": "member"}, - "status": "active", - "created_at": "2024-01-01T00:00:00.000Z", - "updated_at": "2024-01-01T00:00:00.000Z", - }, - "created_at": "2024-01-01T00:00:00.000Z", - } - ], - "list_metadata": { - "after": None, - }, - } - - capture_and_mock_http_client_request( - http_client=module_instance._http_client, - status_code=200, - response_dict=mock_response, - ) - - events: EventsListResource = syncify( - module_instance.list_events(events=["organization_membership.created"]) - ) - - event = events.data[0] - assert isinstance(event, OrganizationMembershipCreatedEvent) - assert event.data.organization_name is None - assert event.data.custom_attributes == {} - - def test_list_events_vault_data_created( - self, - module_instance: Union[Events, AsyncEvents], - capture_and_mock_http_client_request, - ): - mock_response = { - "object": "list", - "data": [ - { - "object": "event", - "id": "event_vault_01", - "event": "vault.data.created", - "data": { - "actor_id": "user_01234", - "actor_source": "dashboard", - "actor_name": "Test User", - "kv_name": "my-secret", - "key_id": "key_01234", - "key_context": {"env": "production"}, - }, - "created_at": "2024-01-01T00:00:00.000Z", - } - ], - "list_metadata": { - "after": None, - }, - } - - capture_and_mock_http_client_request( - http_client=module_instance._http_client, - status_code=200, - response_dict=mock_response, - ) - - events: EventsListResource = syncify( - module_instance.list_events(events=["vault.data.created"]) - ) - - event = events.data[0] - assert isinstance(event, VaultDataCreatedEvent) - assert event.data.actor_id == "user_01234" - assert event.data.actor_source == "dashboard" - assert event.data.actor_name == "Test User" - assert event.data.kv_name == "my-secret" - assert event.data.key_id == "key_01234" - assert event.data.key_context is not None - assert event.data.key_context.root == {"env": "production"} - - def test_list_events_vault_dek_read( - self, - module_instance: Union[Events, AsyncEvents], - capture_and_mock_http_client_request, - ): - mock_response = { - "object": "list", - "data": [ - { - "object": "event", - "id": "event_vault_02", - "event": "vault.dek.read", - "data": { - "actor_id": "user_01234", - "actor_source": "api", - "actor_name": "API Client", - "key_ids": ["key_01", "key_02"], - "key_context": {"tenant": "acme"}, - }, - "created_at": "2024-01-01T00:00:00.000Z", - } - ], - "list_metadata": { - "after": None, - }, - } - - capture_and_mock_http_client_request( - http_client=module_instance._http_client, - status_code=200, - response_dict=mock_response, - ) - - events: EventsListResource = syncify( - module_instance.list_events(events=["vault.dek.read"]) - ) - - event = events.data[0] - assert isinstance(event, VaultDekReadEvent) - assert event.data.key_ids == ["key_01", "key_02"] - assert event.data.key_context is not None - assert event.data.key_context.root == {"tenant": "acme"} - assert event.data.actor_name == "API Client" - - def test_list_events_vault_names_listed( - self, - module_instance: Union[Events, AsyncEvents], - capture_and_mock_http_client_request, - ): - mock_response = { - "object": "list", - "data": [ - { - "object": "event", - "id": "event_vault_03", - "event": "vault.names.listed", - "data": { - "actor_id": "user_01234", - "actor_source": "api", - "actor_name": "Service Account", - }, - "created_at": "2024-01-01T00:00:00.000Z", - } - ], - "list_metadata": { - "after": None, - }, - } - - capture_and_mock_http_client_request( - http_client=module_instance._http_client, - status_code=200, - response_dict=mock_response, - ) - - events: EventsListResource = syncify( - module_instance.list_events(events=["vault.names.listed"]) - ) - - event = events.data[0] - assert isinstance(event, VaultNamesListedEvent) - assert event.data.actor_id == "user_01234" - assert event.data.actor_source == "api" - assert event.data.actor_name == "Service Account" - - def test_list_events_vault_data_read( - self, - module_instance: Union[Events, AsyncEvents], - capture_and_mock_http_client_request, - ): - mock_response = { - "object": "list", - "data": [ - { - "object": "event", - "id": "event_vault_09", - "event": "vault.data.read", - "data": { - "actor_id": "user_01234", - "actor_source": "api", - "actor_name": "Read Service", - "kv_name": "db-password", - "key_id": "key_55", - }, - "created_at": "2024-01-01T00:00:00.000Z", - } - ], - "list_metadata": {"after": None}, - } - - capture_and_mock_http_client_request( - http_client=module_instance._http_client, - status_code=200, - response_dict=mock_response, - ) - - events: EventsListResource = syncify( - module_instance.list_events(events=["vault.data.read"]) - ) - - event = events.data[0] - assert isinstance(event, VaultDataReadEvent) - assert event.data.kv_name == "db-password" - assert event.data.key_id == "key_55" - - def test_list_events_vault_dek_decrypted( - self, - module_instance: Union[Events, AsyncEvents], - capture_and_mock_http_client_request, - ): - mock_response = { - "object": "list", - "data": [ - { - "object": "event", - "id": "event_vault_04", - "event": "vault.dek.decrypted", - "data": { - "actor_id": "user_01234", - "actor_source": "api", - "actor_name": "Decryption Service", - "key_id": "key_99", - }, - "created_at": "2024-01-01T00:00:00.000Z", - } - ], - "list_metadata": {"after": None}, - } - - capture_and_mock_http_client_request( - http_client=module_instance._http_client, - status_code=200, - response_dict=mock_response, - ) - - events: EventsListResource = syncify( - module_instance.list_events(events=["vault.dek.decrypted"]) - ) - - event = events.data[0] - assert isinstance(event, VaultDekDecryptedEvent) - assert event.data.key_id == "key_99" - assert event.data.actor_name == "Decryption Service" - - def test_list_events_vault_kek_created( - self, - module_instance: Union[Events, AsyncEvents], - capture_and_mock_http_client_request, - ): - mock_response = { - "object": "list", - "data": [ - { - "object": "event", - "id": "event_vault_05", - "event": "vault.kek.created", - "data": { - "actor_id": "user_01234", - "actor_source": "dashboard", - "actor_name": "Admin", - "key_name": "production-kek", - "key_id": "kek_01", - }, - "created_at": "2024-01-01T00:00:00.000Z", - } - ], - "list_metadata": {"after": None}, - } - - capture_and_mock_http_client_request( - http_client=module_instance._http_client, - status_code=200, - response_dict=mock_response, - ) - - events: EventsListResource = syncify( - module_instance.list_events(events=["vault.kek.created"]) - ) - - event = events.data[0] - assert isinstance(event, VaultKekCreatedEvent) - assert event.data.key_name == "production-kek" - assert event.data.key_id == "kek_01" - - def test_list_events_vault_data_deleted( - self, - module_instance: Union[Events, AsyncEvents], - capture_and_mock_http_client_request, - ): - mock_response = { - "object": "list", - "data": [ - { - "object": "event", - "id": "event_vault_06", - "event": "vault.data.deleted", - "data": { - "actor_id": "user_01234", - "actor_source": "api", - "actor_name": "Cleanup Job", - "kv_name": "old-secret", - }, - "created_at": "2024-01-01T00:00:00.000Z", - } - ], - "list_metadata": {"after": None}, - } - - capture_and_mock_http_client_request( - http_client=module_instance._http_client, - status_code=200, - response_dict=mock_response, - ) - - events: EventsListResource = syncify( - module_instance.list_events(events=["vault.data.deleted"]) - ) - - event = events.data[0] - assert isinstance(event, VaultDataDeletedEvent) - assert event.data.kv_name == "old-secret" - - def test_list_events_vault_data_updated( - self, - module_instance: Union[Events, AsyncEvents], - capture_and_mock_http_client_request, - ): - mock_response = { - "object": "list", - "data": [ - { - "object": "event", - "id": "event_vault_07", - "event": "vault.data.updated", - "data": { - "actor_id": "user_01234", - "actor_source": "api", - "actor_name": "Rotation Job", - "kv_name": "api-key", - "key_id": "key_02", - "key_context": {"env": "staging"}, - }, - "created_at": "2024-01-01T00:00:00.000Z", - } - ], - "list_metadata": {"after": None}, - } - - capture_and_mock_http_client_request( - http_client=module_instance._http_client, - status_code=200, - response_dict=mock_response, - ) - - events: EventsListResource = syncify( - module_instance.list_events(events=["vault.data.updated"]) - ) - - event = events.data[0] - assert isinstance(event, VaultDataUpdatedEvent) - assert event.data.kv_name == "api-key" - assert event.data.key_id == "key_02" - - def test_list_events_vault_metadata_read( - self, - module_instance: Union[Events, AsyncEvents], - capture_and_mock_http_client_request, - ): - mock_response = { - "object": "list", - "data": [ - { - "object": "event", - "id": "event_vault_08", - "event": "vault.metadata.read", - "data": { - "actor_id": "user_01234", - "actor_source": "api", - "actor_name": "Audit Service", - "kv_name": "config-store", - }, - "created_at": "2024-01-01T00:00:00.000Z", - } - ], - "list_metadata": {"after": None}, - } - - capture_and_mock_http_client_request( - http_client=module_instance._http_client, - status_code=200, - response_dict=mock_response, - ) - - events: EventsListResource = syncify( - module_instance.list_events(events=["vault.metadata.read"]) - ) - - event = events.data[0] - assert isinstance(event, VaultMetadataReadEvent) - assert event.data.kv_name == "config-store" diff --git a/tests/test_fga.py b/tests/test_fga.py deleted file mode 100644 index c0f2337b..00000000 --- a/tests/test_fga.py +++ /dev/null @@ -1,652 +0,0 @@ -import pytest - -from workos.exceptions import ( - AuthenticationException, - BadRequestException, - NotFoundException, - ServerException, -) -from workos.fga import FGA -from workos.types.fga import ( - WarrantCheckInput, - WarrantWrite, - SubjectInput, -) - - -class TestValidation: - @pytest.fixture(autouse=True) - def setup(self, sync_http_client_for_test): - self.http_client = sync_http_client_for_test - self.fga = FGA(http_client=self.http_client) - - def test_get_resource_no_resources(self): - with pytest.raises(ValueError): - self.fga.get_resource(resource_type="", resource_id="test") - - with pytest.raises(ValueError): - self.fga.get_resource(resource_type="test", resource_id="") - - def test_create_resource_no_resources(self): - with pytest.raises(ValueError): - self.fga.create_resource(resource_type="", resource_id="test", meta={}) - - with pytest.raises(ValueError): - self.fga.create_resource(resource_type="test", resource_id="", meta={}) - - def test_update_resource_no_resources(self): - with pytest.raises(ValueError): - self.fga.update_resource(resource_type="", resource_id="test", meta={}) - - with pytest.raises(ValueError): - self.fga.update_resource(resource_type="test", resource_id="", meta={}) - - def test_delete_resource_no_resources(self): - with pytest.raises(ValueError): - self.fga.delete_resource(resource_type="", resource_id="test") - - with pytest.raises(ValueError): - self.fga.delete_resource(resource_type="test", resource_id="") - - def test_batch_write_warrants_no_batch(self): - with pytest.raises(ValueError): - self.fga.batch_write_warrants(batch=[]) - - def test_check_no_checks(self): - with pytest.raises(ValueError): - self.fga.check(op="any_of", checks=[]) - - -class TestErrorHandling: - @pytest.fixture(autouse=True) - def setup(self, sync_http_client_for_test): - self.http_client = sync_http_client_for_test - self.fga = FGA(http_client=self.http_client) - - @pytest.fixture - def mock_404_response(self): - return { - "code": "not_found", - "message": "test message", - "type": "some-type", - "key": "nonexistent-type", - } - - @pytest.fixture - def mock_400_response(self): - return {"code": "invalid_request", "message": "test message"} - - def test_get_resource_404(self, mock_404_response, mock_http_client_with_response): - mock_http_client_with_response(self.http_client, mock_404_response, 404) - - with pytest.raises(NotFoundException): - self.fga.get_resource(resource_type="test", resource_id="test") - - def test_get_resource_400(self, mock_400_response, mock_http_client_with_response): - mock_http_client_with_response(self.http_client, mock_400_response, 400) - - with pytest.raises(BadRequestException): - self.fga.get_resource(resource_type="test", resource_id="test") - - def test_get_resource_500(self, mock_http_client_with_response): - mock_http_client_with_response(self.http_client, status_code=500) - - with pytest.raises(ServerException): - self.fga.get_resource(resource_type="test", resource_id="test") - - def test_get_resource_401(self, mock_http_client_with_response): - mock_http_client_with_response(self.http_client, status_code=401) - - with pytest.raises(AuthenticationException): - self.fga.get_resource(resource_type="test", resource_id="test") - - -class TestWarnings: - @pytest.fixture(autouse=True) - def setup(self, sync_http_client_for_test): - self.http_client = sync_http_client_for_test - self.fga = FGA(http_client=self.http_client) - - def test_check_with_warning(self, mock_http_client_with_response): - mock_response = { - "result": "authorized", - "is_implicit": True, - "warnings": [ - { - "code": "missing_context_keys", - "message": "Missing context keys", - "keys": ["key1", "key2"], - } - ], - } - mock_http_client_with_response(self.http_client, mock_response, 200) - - response = self.fga.check( - op="any_of", - checks=[ - WarrantCheckInput( - resource_type="schedule", - resource_id="schedule-A1", - relation="viewer", - subject=SubjectInput(resource_type="user", resource_id="user-A"), - ) - ], - ) - assert response.dict(exclude_none=True) == mock_response - - def test_query_with_warning(self, mock_http_client_with_response): - mock_response = { - "object": "list", - "data": [ - { - "resource_type": "user", - "resource_id": "richard", - "relation": "member", - "warrant": { - "resource_type": "role", - "resource_id": "developer", - "relation": "member", - "subject": {"resource_type": "user", "resource_id": "richard"}, - }, - "is_implicit": True, - } - ], - "list_metadata": {}, - "warnings": [ - { - "code": "missing_context_keys", - "message": "Missing context keys", - "keys": ["key1", "key2"], - } - ], - } - - mock_http_client_with_response(self.http_client, mock_response, 200) - - response = self.fga.query( - q="select member of type user for permission:view-docs", - order="asc", - warrant_token="warrant_token", - ) - assert response.dict(exclude_none=True) == mock_response - - def test_check_with_generic_warning(self, mock_http_client_with_response): - mock_response = { - "result": "authorized", - "is_implicit": True, - "warnings": [ - { - "code": "generic", - "message": "Generic warning", - } - ], - } - - mock_http_client_with_response(self.http_client, mock_response, 200) - - response = self.fga.check( - op="any_of", - checks=[ - WarrantCheckInput( - resource_type="schedule", - resource_id="schedule-A1", - relation="viewer", - subject=SubjectInput(resource_type="user", resource_id="user-A"), - ) - ], - ) - assert response.dict(exclude_none=True) == mock_response - - -class TestFGA: - @pytest.fixture(autouse=True) - def setup(self, sync_http_client_for_test): - self.http_client = sync_http_client_for_test - self.fga = FGA(http_client=self.http_client) - - @pytest.fixture - def mock_get_resource_response(self): - return { - "resource_type": "test", - "resource_id": "first-resource", - "meta": {"my_key": "my_val"}, - "created_at": "2022-02-15T15:14:19.392Z", - } - - def test_get_resource( - self, mock_get_resource_response, mock_http_client_with_response - ): - mock_http_client_with_response( - self.http_client, mock_get_resource_response, 200 - ) - enroll_factor = self.fga.get_resource( - resource_type=mock_get_resource_response["resource_type"], - resource_id=mock_get_resource_response["resource_id"], - ) - assert enroll_factor.dict(exclude_none=True) == mock_get_resource_response - - @pytest.fixture - def mock_list_resources_response(self): - return { - "object": "list", - "data": [ - { - "resource_type": "test", - "resource_id": "third-resource", - "meta": {"my_key": "my_val"}, - }, - { - "resource_type": "test", - "resource_id": "{{ createResourceWithGeneratedId.resource_id }}", - }, - {"resource_type": "test", "resource_id": "second-resource"}, - {"resource_type": "test", "resource_id": "first-resource"}, - ], - "list_metadata": {}, - } - - def test_list_resources( - self, mock_list_resources_response, mock_http_client_with_response - ): - mock_http_client_with_response( - self.http_client, mock_list_resources_response, 200 - ) - response = self.fga.list_resources() - assert response.dict(exclude_none=True) == mock_list_resources_response - - @pytest.fixture - def mock_create_resource_response(self): - return { - "resource_type": "test", - "resource_id": "third-resource", - "meta": {"my_key": "my_val"}, - } - - def test_create_resource( - self, mock_create_resource_response, mock_http_client_with_response - ): - mock_http_client_with_response( - self.http_client, mock_create_resource_response, 200 - ) - response = self.fga.create_resource( - resource_type=mock_create_resource_response["resource_type"], - resource_id=mock_create_resource_response["resource_id"], - meta=mock_create_resource_response["meta"], - ) - assert response.dict(exclude_none=True) == mock_create_resource_response - - @pytest.fixture - def mock_update_resource_response(self): - return { - "resource_type": "test", - "resource_id": "third-resource", - "meta": {"my_updated_key": "my_updated_value"}, - } - - def test_update_resource( - self, mock_update_resource_response, mock_http_client_with_response - ): - mock_http_client_with_response( - self.http_client, mock_update_resource_response, 200 - ) - response = self.fga.update_resource( - resource_type=mock_update_resource_response["resource_type"], - resource_id=mock_update_resource_response["resource_id"], - meta=mock_update_resource_response["meta"], - ) - assert response.dict(exclude_none=True) == mock_update_resource_response - - def test_delete_resource(self, mock_http_client_with_response): - mock_http_client_with_response(self.http_client, status_code=200) - self.fga.delete_resource(resource_type="test", resource_id="third-resource") - - @pytest.fixture - def mock_list_resource_types_response(self): - return { - "object": "list", - "data": [ - { - "type": "feature", - "relations": { - "member": { - "inherit_if": "any_of", - "rules": [ - { - "inherit_if": "member", - "of_type": "feature", - "with_relation": "member", - }, - { - "inherit_if": "member", - "of_type": "pricing-tier", - "with_relation": "member", - }, - { - "inherit_if": "member", - "of_type": "tenant", - "with_relation": "member", - }, - ], - } - }, - }, - { - "type": "permission", - "relations": { - "member": { - "inherit_if": "any_of", - "rules": [ - { - "inherit_if": "member", - "of_type": "permission", - "with_relation": "member", - }, - { - "inherit_if": "member", - "of_type": "role", - "with_relation": "member", - }, - ], - } - }, - }, - { - "type": "pricing-tier", - "relations": { - "member": { - "inherit_if": "any_of", - "rules": [ - { - "inherit_if": "member", - "of_type": "pricing-tier", - "with_relation": "member", - }, - { - "inherit_if": "member", - "of_type": "tenant", - "with_relation": "member", - }, - ], - } - }, - }, - ], - "list_metadata": {"after": "after_token"}, - } - - def test_list_resource_types( - self, mock_list_resource_types_response, mock_http_client_with_response - ): - mock_http_client_with_response( - self.http_client, mock_list_resource_types_response, 200 - ) - response = self.fga.list_resource_types() - assert response.dict(exclude_none=True) == mock_list_resource_types_response - - @pytest.fixture - def mock_list_warrants_response(self): - return { - "object": "list", - "data": [ - { - "resource_type": "permission", - "resource_id": "view-balance-sheet", - "relation": "member", - "subject": { - "resource_type": "role", - "resource_id": "senior-accountant", - "relation": "member", - }, - }, - { - "resource_type": "permission", - "resource_id": "balance-sheet:edit", - "relation": "member", - "subject": {"resource_type": "user", "resource_id": "user-b"}, - }, - ], - "list_metadata": {}, - } - - def test_list_warrants( - self, mock_list_warrants_response, mock_http_client_with_response - ): - mock_http_client_with_response( - self.http_client, mock_list_warrants_response, 200 - ) - response = self.fga.list_warrants() - assert response.dict(exclude_none=True) == mock_list_warrants_response - - @pytest.fixture - def mock_write_warrant_response(self): - return {"warrant_token": "warrant_token"} - - def test_write_warrant( - self, mock_write_warrant_response, mock_http_client_with_response - ): - mock_http_client_with_response( - self.http_client, mock_write_warrant_response, 200 - ) - - response = self.fga.write_warrant( - op="create", - subject_type="role", - subject_id="senior-accountant", - subject_relation="member", - relation="member", - resource_type="permission", - resource_id="view-balance-sheet", - ) - assert response.dict(exclude_none=True) == mock_write_warrant_response - - def test_batch_write_warrants( - self, mock_write_warrant_response, mock_http_client_with_response - ): - mock_http_client_with_response( - self.http_client, mock_write_warrant_response, 200 - ) - - response = self.fga.batch_write_warrants( - batch=[ - WarrantWrite( - op="create", - resource_type="permission", - resource_id="view-balance-sheet", - relation="member", - subject=SubjectInput( - resource_type="role", - resource_id="senior-accountant", - relation="member", - ), - ), - WarrantWrite( - op="create", - resource_type="permission", - resource_id="balance-sheet:edit", - relation="member", - subject=SubjectInput( - resource_type="user", - resource_id="user-b", - ), - ), - ] - ) - assert response.dict(exclude_none=True) == mock_write_warrant_response - - @pytest.fixture - def mock_check_warrant_response(self): - return {"result": "authorized", "is_implicit": True} - - def test_check(self, mock_check_warrant_response, mock_http_client_with_response): - mock_http_client_with_response( - self.http_client, mock_check_warrant_response, 200 - ) - - response = self.fga.check( - op="any_of", - checks=[ - WarrantCheckInput( - resource_type="schedule", - resource_id="schedule-A1", - relation="viewer", - subject=SubjectInput(resource_type="user", resource_id="user-A"), - ) - ], - ) - assert response.dict(exclude_none=True) == mock_check_warrant_response - - @pytest.fixture - def mock_check_response_with_debug_info(self): - return { - "result": "authorized", - "is_implicit": False, - "debug_info": { - "processing_time": 123, - "decision_tree": { - "check": { - "resource_type": "report", - "resource_id": "report-a", - "relation": "editor", - "subject": {"resource_type": "user", "resource_id": "user-b"}, - "context": {"tenant": "tenant-b"}, - }, - "policy": 'tenant == "tenant-b"', - "decision": "eval_policy", - "processing_time": 123, - "children": [ - { - "check": { - "resource_type": "role", - "resource_id": "admin", - "relation": "member", - "subject": { - "resource_type": "user", - "resource_id": "user-b", - }, - "context": {"tenant": "tenant-b"}, - }, - "policy": 'tenant == "tenant-b"', - "decision": "eval_policy", - "processing_time": 123, - } - ], - }, - }, - } - - def test_check_with_debug_info( - self, mock_check_response_with_debug_info, mock_http_client_with_response - ): - mock_http_client_with_response( - self.http_client, mock_check_response_with_debug_info, 200 - ) - - response = self.fga.check( - op="any_of", - checks=[ - WarrantCheckInput( - resource_type="report", - resource_id="report-a", - relation="editor", - subject=SubjectInput(resource_type="user", resource_id="user-b"), - context={"tenant": "tenant-b"}, - ) - ], - debug=True, - ) - assert response.dict(exclude_none=True) == mock_check_response_with_debug_info - - @pytest.fixture - def mock_batch_check_response(self): - return [ - {"result": "authorized", "is_implicit": True}, - {"result": "not_authorized", "is_implicit": True}, - ] - - def test_check_batch( - self, mock_batch_check_response, mock_http_client_with_response - ): - mock_http_client_with_response(self.http_client, mock_batch_check_response, 200) - - response = self.fga.check_batch( - checks=[ - WarrantCheckInput( - resource_type="schedule", - resource_id="schedule-A1", - relation="viewer", - subject=SubjectInput(resource_type="user", resource_id="user-A"), - ), - WarrantCheckInput( - resource_type="schedule", - resource_id="schedule-A1", - relation="editor", - subject=SubjectInput(resource_type="user", resource_id="user-B"), - ), - ] - ) - - assert [ - r.dict(exclude_none=True) for r in response - ] == mock_batch_check_response - - @pytest.fixture - def mock_query_response(self): - return { - "object": "list", - "data": [ - { - "resource_type": "user", - "resource_id": "richard", - "relation": "member", - "warrant": { - "resource_type": "role", - "resource_id": "developer", - "relation": "member", - "subject": {"resource_type": "user", "resource_id": "richard"}, - }, - "is_implicit": True, - }, - { - "resource_type": "user", - "resource_id": "tom", - "relation": "member", - "warrant": { - "resource_type": "role", - "resource_id": "manager", - "relation": "member", - "subject": {"resource_type": "user", "resource_id": "tom"}, - }, - "is_implicit": True, - }, - ], - "list_metadata": {}, - } - - def test_query(self, mock_query_response, mock_http_client_with_response): - mock_http_client_with_response(self.http_client, mock_query_response, 200) - - response = self.fga.query( - q="select member of type user for permission:view-docs", - order="asc", - warrant_token="warrant_token", - ) - assert response.dict(exclude_none=True) == mock_query_response - - def test_query_with_context( - self, mock_query_response, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_query_response, 200 - ) - - response = self.fga.query( - q="select member of type user for permission:view-docs", - order="asc", - warrant_token="warrant_token", - context={"region": "us", "subscription": "pro"}, - ) - - assert request_kwargs["url"] == "https://api.workos.test/fga/v1/query" - expected_full_url = "https://api.workos.test/fga/v1/query?q=select+member+of+type+user+for+permission%3Aview-docs&limit=10&order=asc&context=%7B%22region%22%3A+%22us%22%2C+%22subscription%22%3A+%22pro%22%7D" - assert request_kwargs["full_url"] == expected_full_url - assert response.dict(exclude_none=True) == mock_query_response diff --git a/tests/test_mfa.py b/tests/test_mfa.py deleted file mode 100644 index bddd5765..00000000 --- a/tests/test_mfa.py +++ /dev/null @@ -1,257 +0,0 @@ -from workos.mfa import Mfa -import pytest - - -class TestMfa(object): - @pytest.fixture(autouse=True) - def setup(self, sync_http_client_for_test): - self.http_client = sync_http_client_for_test - self.mfa = Mfa(http_client=self.http_client) - - @pytest.fixture - def mock_enroll_factor_no_type(self): - return None - - @pytest.fixture - def mock_enroll_factor_incorrect_type(self): - return "dinosaur" - - @pytest.fixture - def mock_enroll_factor_totp_payload(self): - return [ - "totp", - "workos", - "stanley@yelnats.com", - ] - - @pytest.fixture - def mock_enroll_factor_sms_payload(self): - return ["sms", "7208675309"] - - @pytest.fixture - def mock_challenge_factor_payload(self): - return ["auth_factor_01FWRSPQ2XXXQKBCY5AAW5T59W", "Your code is {{code}}"] - - @pytest.fixture - def mock_verify_challenge_payload(self): - return ["auth_challenge_01FWRY5H0XXXX0JGGVTY6QFX7X", "626592"] - - @pytest.fixture - def mock_enroll_factor_response_sms(self): - return { - "object": "authentication_factor", - "id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", - "created_at": "2022-02-15T15:14:19.392Z", - "updated_at": "2022-02-15T15:14:19.392Z", - "type": "sms", - "sms": {"phone_number": "+19204703484"}, - "user_id": None, - } - - @pytest.fixture - def mock_enroll_factor_response_totp(self): - return { - "object": "authentication_factor", - "id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", - "created_at": "2022-02-15T15:14:19.392Z", - "updated_at": "2022-02-15T15:14:19.392Z", - "type": "totp", - "totp": { - "issuer": "FooCorp", - "user": "test@example.com", - "qr_code": "data:image/png;base64,{base64EncodedPng}", - "secret": "NAGCCFS3EYRB422HNAKAKY3XDUORMSRF", - "uri": "otpauth://totp/FooCorp:alan.turing@foo-corp.com?secret=NAGCCFS3EYRB422HNAKAKY3XDUORMSRF&issuer=FooCorp", - }, - "user_id": None, - } - - @pytest.fixture - def mock_get_factor_response_totp(self): - return { - "object": "authentication_factor", - "id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", - "created_at": "2022-02-15T15:14:19.392Z", - "updated_at": "2022-02-15T15:14:19.392Z", - "type": "totp", - "totp": { - "issuer": "FooCorp", - "user": "test@example.com", - }, - "user_id": None, - } - - @pytest.fixture - def mock_challenge_factor_response(self): - return { - "object": "authentication_challenge", - "id": "auth_challenge_01FVYZWQTZQ5VB6BC5MPG2EYC5", - "created_at": "2022-02-15T15:26:53.274Z", - "updated_at": "2022-02-15T15:26:53.274Z", - "expires_at": "2022-02-15T15:36:53.279Z", - "authentication_factor_id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", - "code": None, - } - - @pytest.fixture - def mock_verify_challenge_response(self): - return { - "challenge": { - "object": "authentication_challenge", - "id": "auth_challenge_01FVYZWQTZQ5VB6BC5MPG2EYC5", - "created_at": "2022-02-15T15:26:53.274Z", - "updated_at": "2022-02-15T15:26:53.274Z", - "expires_at": "2022-02-15T15:36:53.279Z", - "authentication_factor_id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", - "code": None, - }, - "valid": True, - } - - def test_enroll_factor_totp_no_issuer(self, mock_enroll_factor_totp_payload): - with pytest.raises(ValueError) as err: - self.mfa.enroll_factor( - type=mock_enroll_factor_totp_payload[0], - totp_issuer=None, - totp_user=mock_enroll_factor_totp_payload[2], - ) - assert ( - "Incomplete arguments. Need to specify both totp_issuer and totp_user when type is totp" - in str(err.value) - ) - - def test_enroll_factor_totp_no_user(self, mock_enroll_factor_totp_payload): - with pytest.raises(ValueError) as err: - self.mfa.enroll_factor( - type=mock_enroll_factor_totp_payload[0], - totp_issuer=mock_enroll_factor_totp_payload[1], - totp_user=None, - ) - assert ( - "Incomplete arguments. Need to specify both totp_issuer and totp_user when type is totp" - in str(err.value) - ) - - def test_enroll_factor_sms_no_phone_number(self, mock_enroll_factor_sms_payload): - with pytest.raises(ValueError) as err: - self.mfa.enroll_factor( - type=mock_enroll_factor_sms_payload[0], phone_number=None - ) - assert ( - "Incomplete arguments. Need to specify phone_number when type is sms" - in str(err.value) - ) - - def test_enroll_factor_sms_success( - self, mock_enroll_factor_response_sms, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_enroll_factor_response_sms, 200 - ) - enroll_factor = self.mfa.enroll_factor(type="sms", phone_number="9204448888") - - assert request_kwargs["url"].endswith("/auth/factors/enroll") - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == { - "type": "sms", - "phone_number": "9204448888", - } - assert enroll_factor.dict() == mock_enroll_factor_response_sms - - def test_enroll_factor_totp_success( - self, mock_enroll_factor_response_totp, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_enroll_factor_response_totp, 200 - ) - enroll_factor = self.mfa.enroll_factor( - type="totp", totp_issuer="testissuer", totp_user="testuser" - ) - - assert request_kwargs["url"].endswith("/auth/factors/enroll") - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == { - "type": "totp", - "totp_issuer": "testissuer", - "totp_user": "testuser", - } - assert enroll_factor.dict() == mock_enroll_factor_response_totp - - def test_get_factor_totp_success( - self, mock_get_factor_response_totp, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_get_factor_response_totp, 200 - ) - authentication_factor_id = mock_get_factor_response_totp["id"] - response = self.mfa.get_factor( - authentication_factor_id=authentication_factor_id - ) - - assert request_kwargs["url"].endswith( - f"/auth/factors/{authentication_factor_id}" - ) - assert request_kwargs["method"] == "get" - assert response.dict() == mock_get_factor_response_totp - - def test_get_factor_sms_success( - self, mock_enroll_factor_response_sms, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_enroll_factor_response_sms, 200 - ) - - authentication_factor_id = mock_enroll_factor_response_sms["id"] - response = self.mfa.get_factor( - authentication_factor_id=authentication_factor_id - ) - - assert request_kwargs["url"].endswith( - f"/auth/factors/{authentication_factor_id}" - ) - assert request_kwargs["method"] == "get" - assert response.dict() == mock_enroll_factor_response_sms - - def test_delete_factor_success(self, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, None, 200 - ) - response = self.mfa.delete_factor("auth_factor_01FZ4TS14D1PHFNZ9GF6YD8M1F") - assert request_kwargs["url"].endswith( - "/auth/factors/auth_factor_01FZ4TS14D1PHFNZ9GF6YD8M1F" - ) - assert request_kwargs["method"] == "delete" - assert response is None - - def test_challenge_success( - self, mock_challenge_factor_response, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_challenge_factor_response, 200 - ) - challenge_factor = self.mfa.challenge_factor( - authentication_factor_id="auth_factor_01FXNWW32G7F3MG8MYK5D1HJJM" - ) - assert request_kwargs["url"].endswith( - "/auth/factors/auth_factor_01FXNWW32G7F3MG8MYK5D1HJJM/challenge" - ) - assert request_kwargs["method"] == "post" - assert challenge_factor.dict() == mock_challenge_factor_response - - def test_verify_success( - self, mock_verify_challenge_response, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_verify_challenge_response, 200 - ) - verify_challenge = self.mfa.verify_challenge( - authentication_challenge_id="auth_challenge_01FXNXH8Y2K3YVWJ10P139A6DT", - code="093647", - ) - - assert request_kwargs["url"].endswith( - "/auth/challenges/auth_challenge_01FXNXH8Y2K3YVWJ10P139A6DT/verify" - ) - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == {"code": "093647"} - assert verify_challenge.dict() == mock_verify_challenge_response diff --git a/tests/test_organization_domains.py b/tests/test_organization_domains.py deleted file mode 100644 index cd61a590..00000000 --- a/tests/test_organization_domains.py +++ /dev/null @@ -1,136 +0,0 @@ -from typing import Union -import pytest -from tests.utils.syncify import syncify -from workos.organization_domains import AsyncOrganizationDomains, OrganizationDomains - - -@pytest.mark.sync_and_async(OrganizationDomains, AsyncOrganizationDomains) -class TestOrganizationDomains: - @pytest.fixture(autouse=True) - def setup( - self, module_instance: Union[OrganizationDomains, AsyncOrganizationDomains] - ): - self.http_client = module_instance._http_client - self.organization_domains = module_instance - - @pytest.fixture - def mock_organization_domain(self): - return { - "object": "organization_domain", - "id": "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8", - "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "domain": "example.com", - "state": "pending", - "verification_strategy": "dns", - "verification_token": "workos_example_verification_token_12345", - "verification_prefix": "_workos-challenge", - "created_at": "2023-01-01T12:00:00.000Z", - "updated_at": "2023-01-01T12:00:00.000Z", - } - - @pytest.fixture - def mock_organization_domain_verified(self): - return { - "object": "organization_domain", - "id": "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8", - "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "domain": "example.com", - "state": "verified", - "verification_strategy": "dns", - "verification_token": "workos_example_verification_token_12345", - "verification_prefix": "_workos-challenge", - "created_at": "2023-01-01T12:00:00.000Z", - "updated_at": "2023-01-01T12:00:00.000Z", - } - - def test_get_organization_domain( - self, capture_and_mock_http_client_request, mock_organization_domain - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, - mock_organization_domain, - 200, - ) - - organization_domain = syncify( - self.organization_domains.get_organization_domain( - organization_domain_id="org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8" - ) - ) - - assert request_kwargs["url"].endswith( - "/organization_domains/org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8" - ) - assert request_kwargs["method"] == "get" - assert organization_domain.id == "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8" - assert organization_domain.domain == "example.com" - assert organization_domain.state == "pending" - assert organization_domain.verification_strategy == "dns" - - def test_create_organization_domain( - self, capture_and_mock_http_client_request, mock_organization_domain - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, - mock_organization_domain, - 201, - ) - - organization_domain = syncify( - self.organization_domains.create_organization_domain( - organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", - domain="example.com", - ) - ) - - assert request_kwargs["url"].endswith("/organization_domains") - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == { - "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "domain": "example.com", - } - assert organization_domain.id == "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8" - assert organization_domain.domain == "example.com" - assert organization_domain.organization_id == "org_01EHT88Z8J8795GZNQ4ZP1J81T" - - def test_verify_organization_domain( - self, capture_and_mock_http_client_request, mock_organization_domain_verified - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, - mock_organization_domain_verified, - 200, - ) - - organization_domain = syncify( - self.organization_domains.verify_organization_domain( - organization_domain_id="org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8" - ) - ) - - assert request_kwargs["url"].endswith( - "/organization_domains/org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8/verify" - ) - assert request_kwargs["method"] == "post" - assert organization_domain.id == "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8" - assert organization_domain.state == "verified" - - def test_delete_organization_domain(self, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, - None, - 204, - headers={"content-type": "text/plain; charset=utf-8"}, - ) - - response = syncify( - self.organization_domains.delete_organization_domain( - organization_domain_id="org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8" - ) - ) - - assert request_kwargs["url"].endswith( - "/organization_domains/org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8" - ) - assert request_kwargs["method"] == "delete" - assert response is None diff --git a/tests/test_organizations.py b/tests/test_organizations.py deleted file mode 100644 index 14dcc980..00000000 --- a/tests/test_organizations.py +++ /dev/null @@ -1,414 +0,0 @@ -import datetime -from typing import Union -import pytest -from tests.types.test_auto_pagination_function import TestAutoPaginationFunction -from tests.utils.fixtures.mock_api_key import MockApiKey, MockApiKeyWithValue -from tests.utils.fixtures.mock_feature_flag import MockFeatureFlag -from tests.utils.fixtures.mock_organization import MockOrganization -from tests.utils.fixtures.mock_role import MockRole -from tests.utils.list_resource import list_response_of -from tests.utils.syncify import syncify -from workos.organizations import AsyncOrganizations, Organizations - - -@pytest.mark.sync_and_async(Organizations, AsyncOrganizations) -class TestOrganizations: - @pytest.fixture(autouse=True) - def setup(self, module_instance: Union[Organizations, AsyncOrganizations]): - self.http_client = module_instance._http_client - self.organizations = module_instance - - @pytest.fixture - def mock_organization(self): - return MockOrganization("org_01EHT88Z8J8795GZNQ4ZP1J81T").dict() - - @pytest.fixture - def mock_organization_updated(self): - return { - "name": "Example Organization", - "object": "organization", - "created_at": datetime.datetime.now().isoformat(), - "updated_at": datetime.datetime.now().isoformat(), - "id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "allow_profiles_outside_organization": True, - "domains": [ - { - "domain": "example.io", - "object": "organization_domain", - "id": "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8", - "state": "verified", - "organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T", - "verification_strategy": "dns", - "verification_token": "token", - "created_at": datetime.datetime.now().isoformat(), - "updated_at": datetime.datetime.now().isoformat(), - } - ], - } - - @pytest.fixture - def mock_organizations(self): - organization_list = [MockOrganization(id=str(i)).dict() for i in range(10)] - - return { - "data": organization_list, - "list_metadata": {"before": None, "after": None}, - "object": "list", - } - - @pytest.fixture - def mock_organizations_single_page_response(self): - organization_list = [MockOrganization(id=str(i)).dict() for i in range(10)] - return { - "data": organization_list, - "list_metadata": {"before": None, "after": None}, - "object": "list", - } - - @pytest.fixture - def mock_organizations_multiple_data_pages(self): - organizations_list = [ - MockOrganization(id=str(f"org_{i + 1}")).dict() for i in range(40) - ] - return list_response_of(data=organizations_list) - - @pytest.fixture - def mock_organization_roles(self): - return { - "data": [MockRole(id=str(i)).dict() for i in range(10)], - "object": "list", - } - - @pytest.fixture - def mock_feature_flags(self): - return { - "data": [MockFeatureFlag(id=f"flag_{str(i)}").dict() for i in range(2)], - "object": "list", - "list_metadata": {"before": None, "after": None}, - } - - def test_list_organizations( - self, mock_organizations, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_organizations, 200 - ) - - organizations_response = syncify(self.organizations.list_organizations()) - - def to_dict(x): - return x.dict() - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/organizations") - assert ( - list(map(to_dict, organizations_response.data)) - == mock_organizations["data"] - ) - - def test_get_organization( - self, mock_organization, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_organization, 200 - ) - - organization = syncify( - self.organizations.get_organization(organization_id="organization_id") - ) - - assert organization.dict() == mock_organization - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/organizations/organization_id") - - def test_get_organization_by_external_id( - self, mock_organization, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_organization, 200 - ) - - organization = syncify( - self.organizations.get_organization_by_external_id(external_id="test") - ) - - assert organization.dict() == mock_organization - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/organizations/external_id/test") - - def test_create_organization_with_domain_data( - self, mock_organization, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_organization, 201 - ) - - payload = { - "domain_data": [{"domain": "example.com", "state": "verified"}], - "name": "Test Organization", - } - organization = syncify(self.organizations.create_organization(**payload)) - - assert organization.id == "org_01EHT88Z8J8795GZNQ4ZP1J81T" - assert organization.name == "Foo Corporation" - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith("/organizations") - assert request_kwargs["json"] == payload - - def test_sends_idempotency_key( - self, mock_organization, capture_and_mock_http_client_request - ): - idempotency_key = "test_123456789" - - payload = { - "domain_data": [{"domain": "example.com", "state": "verified"}], - "name": "Foo Corporation", - } - - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_organization, 200 - ) - - response = syncify( - self.organizations.create_organization( - **payload, idempotency_key=idempotency_key - ) - ) - - assert request_kwargs["headers"]["idempotency-key"] == idempotency_key - assert response.name == "Foo Corporation" - - def test_update_organization_with_domain_data( - self, mock_organization_updated, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_organization_updated, 201 - ) - - updated_organization = syncify( - self.organizations.update_organization( - organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", - domain_data=[{"domain": "example.io", "state": "verified"}], - ) - ) - - assert request_kwargs["url"].endswith( - "/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T" - ) - assert request_kwargs["method"] == "put" - assert request_kwargs["json"] == { - "domain_data": [{"domain": "example.io", "state": "verified"}] - } - assert updated_organization.id == "org_01EHT88Z8J8795GZNQ4ZP1J81T" - assert updated_organization.name == "Example Organization" - domain = updated_organization.domains[0] - assert domain.domain == "example.io" - assert domain.object == "organization_domain" - assert domain.id == "org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8" - assert domain.state == "verified" - assert domain.organization_id == "org_01EHT88Z8J8795GZNQ4ZP1J81T" - assert domain.verification_strategy == "dns" - assert domain.verification_token == "token" - assert domain.verification_prefix is None - assert isinstance(domain.created_at, str) - assert isinstance(domain.updated_at, str) - - def test_delete_organization(self, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, - 202, - headers={"content-type": "text/plain; charset=utf-8"}, - ) - - response = syncify( - self.organizations.delete_organization(organization_id="organization_id") - ) - - assert request_kwargs["url"].endswith("/organizations/organization_id") - assert request_kwargs["method"] == "delete" - assert response is None - - def test_list_organizations_auto_pagination_for_single_page( - self, - mock_organizations_single_page_response, - test_auto_pagination: TestAutoPaginationFunction, - ): - test_auto_pagination( - http_client=self.http_client, - list_function=self.organizations.list_organizations, - expected_all_page_data=mock_organizations_single_page_response["data"], - ) - - def test_list_organizations_auto_pagination_for_multiple_pages( - self, - mock_organizations_multiple_data_pages, - test_auto_pagination: TestAutoPaginationFunction, - ): - test_auto_pagination( - http_client=self.http_client, - list_function=self.organizations.list_organizations, - expected_all_page_data=mock_organizations_multiple_data_pages["data"], - ) - - def test_list_organization_roles( - self, mock_organization_roles, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_organization_roles, 200 - ) - - organization_roles_response = syncify( - self.organizations.list_organization_roles( - organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T" - ) - ) - - def to_dict(x): - return x.dict() - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith( - "/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/roles" - ) - assert ( - list(map(to_dict, organization_roles_response.data)) - == mock_organization_roles["data"] - ) - - def test_list_feature_flags( - self, mock_feature_flags, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_feature_flags, 200 - ) - - feature_flags_response = syncify( - self.organizations.list_feature_flags( - organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T" - ) - ) - - def to_dict(x): - return x.dict() - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith( - "/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/feature-flags" - ) - assert ( - list(map(to_dict, feature_flags_response.data)) - == mock_feature_flags["data"] - ) - - @pytest.fixture - def mock_api_key_with_value(self): - return MockApiKeyWithValue().dict() - - @pytest.fixture - def mock_api_keys(self): - api_key_list = [MockApiKey(id=f"api_key_{i}").dict() for i in range(5)] - return { - "data": api_key_list, - "list_metadata": {"before": None, "after": None}, - "object": "list", - } - - @pytest.fixture - def mock_api_keys_multiple_pages(self): - api_key_list = [MockApiKey(id=f"api_key_{i}").dict() for i in range(40)] - return list_response_of(data=api_key_list) - - def test_create_api_key( - self, mock_api_key_with_value, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_api_key_with_value, 201 - ) - - api_key = syncify( - self.organizations.create_api_key( - organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", - name="Production API Key", - permissions=["posts:read", "posts:write"], - ) - ) - - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith( - "/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/api_keys" - ) - assert request_kwargs["json"]["name"] == "Production API Key" - assert request_kwargs["json"]["permissions"] == ["posts:read", "posts:write"] - assert api_key.id == mock_api_key_with_value["id"] - assert api_key.value == mock_api_key_with_value["value"] - assert api_key.object == "api_key" - - def test_create_api_key_without_permissions( - self, mock_api_key_with_value, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_api_key_with_value, 201 - ) - - api_key = syncify( - self.organizations.create_api_key( - organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", - name="Basic API Key", - ) - ) - - assert request_kwargs["method"] == "post" - assert request_kwargs["json"]["name"] == "Basic API Key" - assert request_kwargs["json"].get("permissions") is None - assert api_key.id == mock_api_key_with_value["id"] - - def test_list_api_keys(self, mock_api_keys, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_api_keys, 200 - ) - - api_keys_response = syncify( - self.organizations.list_api_keys( - organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T" - ) - ) - - def to_dict(x): - return x.dict() - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith( - "/organizations/org_01EHT88Z8J8795GZNQ4ZP1J81T/api_keys" - ) - assert list(map(to_dict, api_keys_response.data)) == mock_api_keys["data"] - - def test_list_api_keys_with_pagination_params( - self, mock_api_keys, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_api_keys, 200 - ) - - syncify( - self.organizations.list_api_keys( - organization_id="org_01EHT88Z8J8795GZNQ4ZP1J81T", - limit=5, - order="asc", - ) - ) - - assert request_kwargs["params"]["limit"] == 5 - assert request_kwargs["params"]["order"] == "asc" - - def test_list_api_keys_auto_pagination_for_multiple_pages( - self, - mock_api_keys_multiple_pages, - test_auto_pagination: TestAutoPaginationFunction, - ): - test_auto_pagination( - http_client=self.http_client, - list_function=self.organizations.list_api_keys, - expected_all_page_data=mock_api_keys_multiple_pages["data"], - list_function_params={"organization_id": "org_01EHT88Z8J8795GZNQ4ZP1J81T"}, - url_path_keys=["organization_id"], - ) diff --git a/tests/test_passwordless.py b/tests/test_passwordless.py deleted file mode 100644 index 053a4bef..00000000 --- a/tests/test_passwordless.py +++ /dev/null @@ -1,57 +0,0 @@ -import pytest - -from workos.passwordless import Passwordless - - -class TestPasswordless: - @pytest.fixture(autouse=True) - def setup(self, sync_http_client_for_test): - self.http_client = sync_http_client_for_test - self.passwordless = Passwordless(http_client=self.http_client) - - @pytest.fixture - def mock_passwordless_session(self): - return { - "id": "passwordless_session_01EHDAK2BFGWCSZXP9HGZ3VK8C", - "email": "demo@workos-okta.com", - "expires_at": "2020-08-13T05:50:00.000Z", - "link": "https://auth.workos.com/passwordless/4TeRexuejWCKs9rrFOIuLRYEr/confirm", - "object": "passwordless_session", - } - - def test_create_session_succeeds( - self, mock_passwordless_session, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_passwordless_session, 201 - ) - - session_options = { - "email": "demo@workos-okta.com", - "type": "MagicLink", - "expires_in": 300, - } - passwordless_session = self.passwordless.create_session(**session_options) - - assert request_kwargs["url"].endswith("/passwordless/sessions") - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == session_options - assert passwordless_session.dict() == mock_passwordless_session - - def test_get_send_session_succeeds(self, capture_and_mock_http_client_request): - response = { - "success": True, - } - request_kwargs = capture_and_mock_http_client_request( - self.http_client, response, 200 - ) - - response = self.passwordless.send_session( - "passwordless_session_01EHDAK2BFGWCSZXP9HGZ3VK8C" - ) - - assert request_kwargs["url"].endswith( - "/passwordless/sessions/passwordless_session_01EHDAK2BFGWCSZXP9HGZ3VK8C/send" - ) - assert request_kwargs["method"] == "post" - assert response diff --git a/tests/test_pipes.py b/tests/test_pipes.py deleted file mode 100644 index 90c2be32..00000000 --- a/tests/test_pipes.py +++ /dev/null @@ -1,167 +0,0 @@ -import pytest - -from tests.utils.syncify import syncify -from workos.pipes import AsyncPipes, Pipes - - -@pytest.mark.sync_and_async(Pipes, AsyncPipes) -class TestPipes: - @pytest.fixture - def mock_access_token(self): - return { - "object": "access_token", - "access_token": "test_access_token_123", - "expires_at": "2026-01-09T12:00:00.000Z", - "scopes": ["read:users", "write:users"], - "missing_scopes": [], - } - - def test_get_access_token_success_with_expiry( - self, - module_instance, - mock_access_token, - capture_and_mock_http_client_request, - ): - response_body = { - "active": True, - "access_token": mock_access_token, - } - request_kwargs = capture_and_mock_http_client_request( - module_instance._http_client, response_body, 200 - ) - - result = syncify( - module_instance.get_access_token( - provider="test-provider", - user_id="user_123", - ) - ) - - assert request_kwargs["url"].endswith("data-integrations/test-provider/token") - assert request_kwargs["method"] == "post" - assert request_kwargs["json"]["user_id"] == "user_123" - assert result.active is True - assert result.access_token.access_token == mock_access_token["access_token"] - assert result.access_token.scopes == mock_access_token["scopes"] - - def test_get_access_token_success_without_expiry( - self, - module_instance, - capture_and_mock_http_client_request, - ): - response_body = { - "active": True, - "access_token": { - "object": "access_token", - "access_token": "test_token", - "expires_at": None, - "scopes": ["read"], - "missing_scopes": [], - }, - } - capture_and_mock_http_client_request( - module_instance._http_client, response_body, 200 - ) - - result = syncify( - module_instance.get_access_token( - provider="test-provider", - user_id="user_123", - ) - ) - - assert result.active is True - assert result.access_token.expires_at is None - - def test_get_access_token_with_organization_id( - self, - module_instance, - mock_access_token, - capture_and_mock_http_client_request, - ): - response_body = { - "active": True, - "access_token": mock_access_token, - } - request_kwargs = capture_and_mock_http_client_request( - module_instance._http_client, response_body, 200 - ) - - syncify( - module_instance.get_access_token( - provider="test-provider", - user_id="user_123", - organization_id="org_456", - ) - ) - - assert request_kwargs["json"]["organization_id"] == "org_456" - - def test_get_access_token_without_organization_id( - self, - module_instance, - mock_access_token, - capture_and_mock_http_client_request, - ): - response_body = { - "active": True, - "access_token": mock_access_token, - } - request_kwargs = capture_and_mock_http_client_request( - module_instance._http_client, response_body, 200 - ) - - syncify( - module_instance.get_access_token( - provider="test-provider", - user_id="user_123", - ) - ) - - assert "organization_id" not in request_kwargs["json"] - - def test_get_access_token_not_installed( - self, - module_instance, - capture_and_mock_http_client_request, - ): - response_body = { - "active": False, - "error": "not_installed", - } - capture_and_mock_http_client_request( - module_instance._http_client, response_body, 200 - ) - - result = syncify( - module_instance.get_access_token( - provider="test-provider", - user_id="user_123", - ) - ) - - assert result.active is False - assert result.error == "not_installed" - - def test_get_access_token_needs_reauthorization( - self, - module_instance, - capture_and_mock_http_client_request, - ): - response_body = { - "active": False, - "error": "needs_reauthorization", - } - capture_and_mock_http_client_request( - module_instance._http_client, response_body, 200 - ) - - result = syncify( - module_instance.get_access_token( - provider="test-provider", - user_id="user_123", - ) - ) - - assert result.active is False - assert result.error == "needs_reauthorization" diff --git a/tests/test_portal.py b/tests/test_portal.py deleted file mode 100644 index 64d0fbcc..00000000 --- a/tests/test_portal.py +++ /dev/null @@ -1,117 +0,0 @@ -import pytest - -from workos.portal import Portal - - -class TestPortal(object): - @pytest.fixture(autouse=True) - def setup(self, sync_http_client_for_test): - self.http_client = sync_http_client_for_test - self.portal = Portal(http_client=self.http_client) - - @pytest.fixture - def mock_portal_link(self): - return {"link": "https://id.workos.com/portal/launch?secret=secret"} - - def test_generate_link_sso( - self, mock_portal_link, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_portal_link, 201 - ) - - response = self.portal.generate_link( - intent="sso", - organization_id="org_01EHQMYV6MBK39QC5PZXHY59C3", - intent_options={"sso": {"bookmark_slug": "my_app"}}, - ) - - assert request_kwargs["url"].endswith("/portal/generate_link") - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == { - "intent": "sso", - "organization": "org_01EHQMYV6MBK39QC5PZXHY59C3", - "intent_options": {"sso": {"bookmark_slug": "my_app"}}, - } - assert response.link == "https://id.workos.com/portal/launch?secret=secret" - - def test_generate_link_domain_verification( - self, mock_portal_link, mock_http_client_with_response - ): - mock_http_client_with_response(self.http_client, mock_portal_link, 201) - - response = self.portal.generate_link( - intent="domain_verification", - organization_id="org_01EHQMYV6MBK39QC5PZXHY59C3", - ) - - assert response.link == "https://id.workos.com/portal/launch?secret=secret" - - def test_generate_link_dsync( - self, mock_portal_link, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_portal_link, 201 - ) - - response = self.portal.generate_link( - intent="dsync", organization_id="org_01EHQMYV6MBK39QC5PZXHY59C3" - ) - - assert request_kwargs["url"].endswith("/portal/generate_link") - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == { - "intent": "dsync", - "organization": "org_01EHQMYV6MBK39QC5PZXHY59C3", - } - assert response.link == "https://id.workos.com/portal/launch?secret=secret" - - def test_generate_link_audit_logs( - self, mock_portal_link, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_portal_link, 201 - ) - - response = self.portal.generate_link( - intent="audit_logs", organization_id="org_01EHQMYV6MBK39QC5PZXHY59C3" - ) - - assert request_kwargs["url"].endswith("/portal/generate_link") - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == { - "intent": "audit_logs", - "organization": "org_01EHQMYV6MBK39QC5PZXHY59C3", - } - assert response.link == "https://id.workos.com/portal/launch?secret=secret" - - def test_generate_link_log_streams( - self, mock_portal_link, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_portal_link, 201 - ) - - response = self.portal.generate_link( - intent="log_streams", organization_id="org_01EHQMYV6MBK39QC5PZXHY59C3" - ) - - assert request_kwargs["url"].endswith("/portal/generate_link") - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == { - "intent": "log_streams", - "organization": "org_01EHQMYV6MBK39QC5PZXHY59C3", - } - assert response.link == "https://id.workos.com/portal/launch?secret=secret" - - def test_generate_link_certificate_renewal( - self, mock_portal_link, mock_http_client_with_response - ): - mock_http_client_with_response(self.http_client, mock_portal_link, 201) - - response = self.portal.generate_link( - intent="certificate_renewal", - organization_id="org_01EHQMYV6MBK39QC5PZXHY59C3", - ) - - assert response.link == "https://id.workos.com/portal/launch?secret=secret" diff --git a/tests/test_session.py b/tests/test_session.py deleted file mode 100644 index 9894b9b7..00000000 --- a/tests/test_session.py +++ /dev/null @@ -1,794 +0,0 @@ -import concurrent.futures -from datetime import datetime, timezone -import time -from unittest.mock import AsyncMock, Mock, patch - -import jwt -import pytest -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import rsa - -from tests.conftest import with_jwks_mock -from workos.session import AsyncSession, Session, _get_jwks_client -from workos.types.user_management.authentication_response import ( - RefreshTokenAuthenticationResponse, -) -from workos.types.user_management.session import ( - AuthenticateWithSessionCookieFailureReason, - AuthenticateWithSessionCookieSuccessResponse, - RefreshWithSessionCookieErrorResponse, - RefreshWithSessionCookieSuccessResponse, -) - - -class SessionFixtures: - @pytest.fixture(autouse=True) - def clear_jwks_cache(self): - _get_jwks_client.cache_clear() - yield - _get_jwks_client.cache_clear() - - @pytest.fixture - def session_constants(self): - # Generate RSA key pair for testing - private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) - - public_key = private_key.public_key() - - # Get the private key in PEM format - private_pem = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption(), - ) - - current_datetime = datetime.now(timezone.utc) - current_timestamp = str(current_datetime) - - token_claims = { - "sid": "session_123", - "org_id": "organization_123", - "role": "admin", - "roles": ["admin"], - "permissions": ["read"], - "entitlements": ["feature_1"], - "feature_flags": ["flag1", "flag2"], - "exp": int(current_datetime.timestamp()) + 3600, - "iat": int(current_datetime.timestamp()), - } - - user_id = "user_123" - - return { - "COOKIE_PASSWORD": "pfSqwTFXUTGEBBD1RQh2kt/oNJYxBgaoZan4Z8sMrKU=", - "SESSION_DATA": "session_data", - "CLIENT_ID": "client_123", - "USER_ID": user_id, - "SESSION_ID": "session_123", - "ORGANIZATION_ID": "organization_123", - "CURRENT_DATETIME": current_datetime, - "CURRENT_TIMESTAMP": current_timestamp, - "PRIVATE_KEY": private_pem, - "PUBLIC_KEY": public_key, - "TEST_TOKEN": jwt.encode(token_claims, private_pem, algorithm="RS256"), - "TEST_TOKEN_CLAIMS": token_claims, - "TEST_USER": { - "object": "user", - "id": user_id, - "email": "user@example.com", - "first_name": "Test", - "last_name": "User", - "email_verified": True, - "created_at": current_timestamp, - "updated_at": current_timestamp, - }, - } - - @pytest.fixture - def mock_user_management(self): - mock = Mock() - mock.get_jwks_url.return_value = ( - "https://api.workos.com/user_management/sso/jwks/client_123" - ) - - return mock - - -class TestSessionBase(SessionFixtures): - @with_jwks_mock - def test_initialize_session_module(self, session_constants, mock_user_management): - session = Session( - user_management=mock_user_management, - client_id=session_constants["CLIENT_ID"], - session_data=session_constants["SESSION_DATA"], - cookie_password=session_constants["COOKIE_PASSWORD"], - ) - - assert session.client_id == session_constants["CLIENT_ID"] - assert session.cookie_password is not None - - @with_jwks_mock - def test_initialize_without_cookie_password( - self, session_constants, mock_user_management - ): - with pytest.raises(ValueError, match="cookie_password is required"): - Session( - user_management=mock_user_management, - client_id=session_constants["CLIENT_ID"], - session_data=session_constants["SESSION_DATA"], - cookie_password="", - ) - - @with_jwks_mock - def test_authenticate_no_session_cookie_provided( - self, session_constants, mock_user_management - ): - session = Session( - user_management=mock_user_management, - client_id=session_constants["CLIENT_ID"], - session_data="", - cookie_password=session_constants["COOKIE_PASSWORD"], - ) - - response = session.authenticate() - - assert response.authenticated is False - assert ( - response.reason - == AuthenticateWithSessionCookieFailureReason.NO_SESSION_COOKIE_PROVIDED - ) - - @with_jwks_mock - def test_authenticate_invalid_session_cookie( - self, session_constants, mock_user_management - ): - session = Session( - user_management=mock_user_management, - client_id=session_constants["CLIENT_ID"], - session_data="invalid_session_data", - cookie_password=session_constants["COOKIE_PASSWORD"], - ) - - response = session.authenticate() - - assert response.authenticated is False - assert ( - response.reason - == AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE - ) - - @with_jwks_mock - def test_authenticate_invalid_jwt(self, session_constants, mock_user_management): - invalid_session_data = Session.seal_data( - {"access_token": "invalid_session_data"}, - session_constants["COOKIE_PASSWORD"], - ) - session = Session( - user_management=mock_user_management, - client_id=session_constants["CLIENT_ID"], - session_data=invalid_session_data, - cookie_password=session_constants["COOKIE_PASSWORD"], - ) - - response = session.authenticate() - assert response.authenticated is False - assert response.reason == AuthenticateWithSessionCookieFailureReason.INVALID_JWT - - @with_jwks_mock - def test_authenticate_jwt_with_aud_claim( - self, session_constants, mock_user_management - ): - access_token = jwt.encode( - { - **session_constants["TEST_TOKEN_CLAIMS"], - **{"aud": session_constants["CLIENT_ID"]}, - }, - session_constants["PRIVATE_KEY"], - algorithm="RS256", - ) - - session_data = Session.seal_data( - {"access_token": access_token, "user": session_constants["TEST_USER"]}, - session_constants["COOKIE_PASSWORD"], - ) - session = Session( - user_management=mock_user_management, - client_id=session_constants["CLIENT_ID"], - session_data=session_data, - cookie_password=session_constants["COOKIE_PASSWORD"], - ) - - response = session.authenticate() - - assert isinstance(response, AuthenticateWithSessionCookieSuccessResponse) - - @with_jwks_mock - def test_authenticate_success(self, session_constants, mock_user_management): - session = Session( - user_management=mock_user_management, - client_id=session_constants["CLIENT_ID"], - session_data=session_constants["SESSION_DATA"], - cookie_password=session_constants["COOKIE_PASSWORD"], - ) - - # Mock the session data that would be unsealed - mock_session = { - "access_token": jwt.encode( - { - "sid": session_constants["SESSION_ID"], - "org_id": session_constants["ORGANIZATION_ID"], - "role": "admin", - "roles": ["admin"], - "permissions": ["read"], - "entitlements": ["feature_1"], - "exp": int(datetime.now(timezone.utc).timestamp()) + 3600, - "iat": int(datetime.now(timezone.utc).timestamp()), - }, - session_constants["PRIVATE_KEY"], - algorithm="RS256", - ), - "user": { - "object": "user", - "id": session_constants["USER_ID"], - "email": "user@example.com", - "email_verified": True, - "created_at": session_constants["CURRENT_TIMESTAMP"], - "updated_at": session_constants["CURRENT_TIMESTAMP"], - }, - "impersonator": None, - } - - # Mock the JWT payload that would be decoded - mock_jwt_payload = { - "sid": session_constants["SESSION_ID"], - "org_id": session_constants["ORGANIZATION_ID"], - "role": "admin", - "roles": ["admin"], - "permissions": ["read"], - "entitlements": ["feature_1"], - "feature_flags": ["flag1", "flag2"], - } - - with ( - patch.object(Session, "unseal_data", return_value=mock_session), - patch("jwt.decode", return_value=mock_jwt_payload), - patch.object( - session.jwks, - "get_signing_key_from_jwt", - return_value=Mock(key=session_constants["PUBLIC_KEY"]), - ), - ): - response = session.authenticate() - - assert isinstance(response, AuthenticateWithSessionCookieSuccessResponse) - assert response.authenticated is True - assert response.session_id == session_constants["SESSION_ID"] - assert response.organization_id == session_constants["ORGANIZATION_ID"] - assert response.role == "admin" - assert response.roles == ["admin"] - assert response.permissions == ["read"] - assert response.entitlements == ["feature_1"] - assert response.feature_flags == ["flag1", "flag2"] - assert response.user.id == session_constants["USER_ID"] - assert response.impersonator is None - - @with_jwks_mock - def test_authenticate_success_with_roles( - self, session_constants, mock_user_management - ): - session = Session( - user_management=mock_user_management, - client_id=session_constants["CLIENT_ID"], - session_data=session_constants["SESSION_DATA"], - cookie_password=session_constants["COOKIE_PASSWORD"], - ) - - # Mock the session data that would be unsealed - mock_session = { - "access_token": jwt.encode( - { - "sid": session_constants["SESSION_ID"], - "org_id": session_constants["ORGANIZATION_ID"], - "role": "admin", - "roles": ["admin", "member"], - "permissions": ["read", "write"], - "entitlements": ["feature_1"], - "exp": int(datetime.now(timezone.utc).timestamp()) + 3600, - "iat": int(datetime.now(timezone.utc).timestamp()), - }, - session_constants["PRIVATE_KEY"], - algorithm="RS256", - ), - "user": { - "object": "user", - "id": session_constants["USER_ID"], - "email": "user@example.com", - "email_verified": True, - "created_at": session_constants["CURRENT_TIMESTAMP"], - "updated_at": session_constants["CURRENT_TIMESTAMP"], - }, - "impersonator": None, - } - - # Mock the JWT payload that would be decoded - mock_jwt_payload = { - "sid": session_constants["SESSION_ID"], - "org_id": session_constants["ORGANIZATION_ID"], - "role": "admin", - "roles": ["admin", "member"], - "permissions": ["read", "write"], - "entitlements": ["feature_1"], - "feature_flags": ["flag1", "flag2"], - } - - with ( - patch.object(Session, "unseal_data", return_value=mock_session), - patch("jwt.decode", return_value=mock_jwt_payload), - patch.object( - session.jwks, - "get_signing_key_from_jwt", - return_value=Mock(key=session_constants["PUBLIC_KEY"]), - ), - ): - response = session.authenticate() - - assert isinstance(response, AuthenticateWithSessionCookieSuccessResponse) - assert response.authenticated is True - assert response.session_id == session_constants["SESSION_ID"] - assert response.organization_id == session_constants["ORGANIZATION_ID"] - assert response.role == "admin" - assert response.roles == ["admin", "member"] - assert response.permissions == ["read", "write"] - assert response.entitlements == ["feature_1"] - assert response.feature_flags == ["flag1", "flag2"] - assert response.user.id == session_constants["USER_ID"] - assert response.impersonator is None - - @with_jwks_mock - def test_refresh_invalid_session_cookie( - self, session_constants, mock_user_management - ): - session = Session( - user_management=mock_user_management, - client_id=session_constants["CLIENT_ID"], - session_data="invalid_session_data", - cookie_password=session_constants["COOKIE_PASSWORD"], - ) - - response = session.refresh() - - assert isinstance(response, RefreshWithSessionCookieErrorResponse) - assert ( - response.reason - == AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE - ) - - def test_seal_data(self, session_constants): - test_data = {"test": "data"} - sealed = Session.seal_data(test_data, session_constants["COOKIE_PASSWORD"]) - assert isinstance(sealed, str) - - # Test unsealing - unsealed = Session.unseal_data(sealed, session_constants["COOKIE_PASSWORD"]) - - assert unsealed == test_data - - def test_unseal_invalid_data(self, session_constants): - with pytest.raises( - Exception - ): # Adjust exception type based on your implementation - Session.unseal_data( - "invalid_sealed_data", session_constants["COOKIE_PASSWORD"] - ) - - -class TestSession(SessionFixtures): - @with_jwks_mock - def test_refresh_success(self, session_constants, mock_user_management): - session_data = Session.seal_data( - { - "refresh_token": "refresh_token_12345", - "user": session_constants["TEST_USER"], - }, - session_constants["COOKIE_PASSWORD"], - ) - - mock_response = { - "access_token": session_constants["TEST_TOKEN"], - "refresh_token": "refresh_token_123", - "sealed_session": session_data, - "user": session_constants["TEST_USER"], - } - - mock_user_management.authenticate_with_refresh_token.return_value = ( - RefreshTokenAuthenticationResponse(**mock_response) - ) - - session = Session( - user_management=mock_user_management, - client_id=session_constants["CLIENT_ID"], - session_data=session_data, - cookie_password=session_constants["COOKIE_PASSWORD"], - ) - - with patch( - "jwt.decode", - return_value={ - "sid": session_constants["SESSION_ID"], - "org_id": session_constants["ORGANIZATION_ID"], - "role": "admin", - "roles": ["admin"], - "permissions": ["read"], - "entitlements": ["feature_1"], - "feature_flags": ["flag1", "flag2"], - }, - ): - response = session.refresh() - - assert isinstance(response, RefreshWithSessionCookieSuccessResponse) - assert response.authenticated is True - assert response.user.id == session_constants["TEST_USER"]["id"] - - # Verify the refresh token was used correctly - mock_user_management.authenticate_with_refresh_token.assert_called_once_with( - refresh_token="refresh_token_12345", - organization_id=None, - session={ - "seal_session": True, - "cookie_password": session_constants["COOKIE_PASSWORD"], - }, - ) - - @with_jwks_mock - def test_refresh_success_with_aud_claim( - self, session_constants, mock_user_management - ): - session_data = Session.seal_data( - { - "refresh_token": "refresh_token_12345", - "user": session_constants["TEST_USER"], - }, - session_constants["COOKIE_PASSWORD"], - ) - - access_token = jwt.encode( - { - **session_constants["TEST_TOKEN_CLAIMS"], - **{"aud": session_constants["CLIENT_ID"]}, - }, - session_constants["PRIVATE_KEY"], - algorithm="RS256", - ) - - mock_response = { - "access_token": access_token, - "refresh_token": "refresh_token_123", - "sealed_session": session_data, - "user": session_constants["TEST_USER"], - } - - mock_user_management.authenticate_with_refresh_token.return_value = ( - RefreshTokenAuthenticationResponse(**mock_response) - ) - - session = Session( - user_management=mock_user_management, - client_id=session_constants["CLIENT_ID"], - session_data=session_data, - cookie_password=session_constants["COOKIE_PASSWORD"], - ) - - response = session.refresh() - - assert isinstance(response, RefreshWithSessionCookieSuccessResponse) - - @with_jwks_mock - def test_authenticate_with_slightly_expired_jwt_fails_without_leeway( - self, session_constants, mock_user_management - ): - # Create a token that's expired by 5 seconds - current_time = int(time.time()) - - # Create token claims with exp 5 seconds in the past - token_claims = { - **session_constants["TEST_TOKEN_CLAIMS"], - "exp": current_time - 5, # Expired by 5 seconds - "iat": current_time - 60, # Issued 60 seconds ago - } - - slightly_expired_token = jwt.encode( - token_claims, - session_constants["PRIVATE_KEY"], - algorithm="RS256", - ) - - # Prepare sealed session data with the slightly expired token - session_data = Session.seal_data( - { - "access_token": slightly_expired_token, - "user": session_constants["TEST_USER"], - }, - session_constants["COOKIE_PASSWORD"], - ) - - # With default leeway=0, authentication should fail - session = Session( - user_management=mock_user_management, - client_id=session_constants["CLIENT_ID"], - session_data=session_data, - cookie_password=session_constants["COOKIE_PASSWORD"], - jwt_leeway=0, - ) - - response = session.authenticate() - assert response.authenticated is False - assert response.reason == AuthenticateWithSessionCookieFailureReason.INVALID_JWT - - @with_jwks_mock - def test_authenticate_with_slightly_expired_jwt_succeeds_with_leeway( - self, session_constants, mock_user_management - ): - # Create a token that's expired by 5 seconds - current_time = int(time.time()) - - # Create token claims with exp 5 seconds in the past - token_claims = { - **session_constants["TEST_TOKEN_CLAIMS"], - "exp": current_time - 5, # Expired by 5 seconds - "iat": current_time - 60, # Issued 60 seconds ago - } - - slightly_expired_token = jwt.encode( - token_claims, - session_constants["PRIVATE_KEY"], - algorithm="RS256", - ) - - # Prepare sealed session data with the slightly expired token - session_data = Session.seal_data( - { - "access_token": slightly_expired_token, - "user": session_constants["TEST_USER"], - }, - session_constants["COOKIE_PASSWORD"], - ) - - # With leeway=10, authentication should succeed - session = Session( - user_management=mock_user_management, - client_id=session_constants["CLIENT_ID"], - session_data=session_data, - cookie_password=session_constants["COOKIE_PASSWORD"], - jwt_leeway=10, # 10 seconds leeway - ) - - response = session.authenticate() - assert response.authenticated is True - assert response.session_id == session_constants["TEST_TOKEN_CLAIMS"]["sid"] - - @with_jwks_mock - def test_authenticate_with_significantly_expired_jwt_fails_without_leeway( - self, session_constants, mock_user_management - ): - # Create a token that's expired by 60 seconds - current_time = int(time.time()) - - # Create token claims with exp 60 seconds in the past - token_claims = { - **session_constants["TEST_TOKEN_CLAIMS"], - "exp": current_time - 60, # Expired by 60 seconds - "iat": current_time - 120, # Issued 120 seconds ago - } - - significantly_expired_token = jwt.encode( - token_claims, - session_constants["PRIVATE_KEY"], - algorithm="RS256", - ) - - # Prepare sealed session data with the significantly expired token - session_data = Session.seal_data( - { - "access_token": significantly_expired_token, - "user": session_constants["TEST_USER"], - }, - session_constants["COOKIE_PASSWORD"], - ) - - # With default leeway=0, authentication should fail - session = Session( - user_management=mock_user_management, - client_id=session_constants["CLIENT_ID"], - session_data=session_data, - cookie_password=session_constants["COOKIE_PASSWORD"], - jwt_leeway=0, - ) - - response = session.authenticate() - assert response.authenticated is False - assert response.reason == AuthenticateWithSessionCookieFailureReason.INVALID_JWT - - @with_jwks_mock - def test_authenticate_with_significantly_expired_jwt_fails_with_insufficient_leeway( - self, session_constants, mock_user_management - ): - # Create a token that's expired by 60 seconds - current_time = int(time.time()) - - # Create token claims with exp 60 seconds in the past - token_claims = { - **session_constants["TEST_TOKEN_CLAIMS"], - "exp": current_time - 60, # Expired by 60 seconds - "iat": current_time - 120, # Issued 120 seconds ago - } - - significantly_expired_token = jwt.encode( - token_claims, - session_constants["PRIVATE_KEY"], - algorithm="RS256", - ) - - # Prepare sealed session data with the significantly expired token - session_data = Session.seal_data( - { - "access_token": significantly_expired_token, - "user": session_constants["TEST_USER"], - }, - session_constants["COOKIE_PASSWORD"], - ) - - # With leeway=10, authentication should still fail (not enough leeway) - session = Session( - user_management=mock_user_management, - client_id=session_constants["CLIENT_ID"], - session_data=session_data, - cookie_password=session_constants["COOKIE_PASSWORD"], - jwt_leeway=10, # 10 seconds leeway is not enough for 60 seconds expiration - ) - - response = session.authenticate() - assert response.authenticated is False - assert response.reason == AuthenticateWithSessionCookieFailureReason.INVALID_JWT - - -class TestAsyncSession(SessionFixtures): - @pytest.mark.asyncio - @with_jwks_mock - async def test_refresh_success(self, session_constants, mock_user_management): - session_data = AsyncSession.seal_data( - { - "refresh_token": "refresh_token_12345", - "user": session_constants["TEST_USER"], - }, - session_constants["COOKIE_PASSWORD"], - ) - - mock_response = { - "access_token": session_constants["TEST_TOKEN"], - "refresh_token": "refresh_token_123", - "sealed_session": session_data, - "user": session_constants["TEST_USER"], - } - - mock_user_management.authenticate_with_refresh_token = AsyncMock( - return_value=(RefreshTokenAuthenticationResponse(**mock_response)) - ) - - session = AsyncSession( - user_management=mock_user_management, - client_id=session_constants["CLIENT_ID"], - session_data=session_data, - cookie_password=session_constants["COOKIE_PASSWORD"], - ) - - with patch( - "jwt.decode", - return_value={ - "sid": session_constants["SESSION_ID"], - "org_id": session_constants["ORGANIZATION_ID"], - "role": "admin", - "roles": ["admin"], - "permissions": ["read"], - "entitlements": ["feature_1"], - "feature_flags": ["flag1", "flag2"], - }, - ): - response = await session.refresh() - - assert isinstance(response, RefreshWithSessionCookieSuccessResponse) - assert response.authenticated is True - assert response.user.id == session_constants["TEST_USER"]["id"] - - # Verify the refresh token was used correctly - mock_user_management.authenticate_with_refresh_token.assert_called_once_with( - refresh_token="refresh_token_12345", - organization_id=None, - session={ - "seal_session": True, - "cookie_password": session_constants["COOKIE_PASSWORD"], - }, - ) - - @pytest.mark.asyncio - @with_jwks_mock - async def test_refresh_success_with_aud_claim( - self, session_constants, mock_user_management - ): - session_data = AsyncSession.seal_data( - { - "refresh_token": "refresh_token_12345", - "user": session_constants["TEST_USER"], - }, - session_constants["COOKIE_PASSWORD"], - ) - - access_token = jwt.encode( - { - **session_constants["TEST_TOKEN_CLAIMS"], - **{"aud": session_constants["CLIENT_ID"]}, - }, - session_constants["PRIVATE_KEY"], - algorithm="RS256", - ) - - mock_response = { - "access_token": access_token, - "refresh_token": "refresh_token_123", - "sealed_session": session_data, - "user": session_constants["TEST_USER"], - } - - mock_user_management.authenticate_with_refresh_token = AsyncMock( - return_value=(RefreshTokenAuthenticationResponse(**mock_response)) - ) - - session = AsyncSession( - user_management=mock_user_management, - client_id=session_constants["CLIENT_ID"], - session_data=session_data, - cookie_password=session_constants["COOKIE_PASSWORD"], - ) - - response = await session.refresh() - - assert isinstance(response, RefreshWithSessionCookieSuccessResponse) - - -class TestJWKSCaching: - def test_jwks_client_caching_same_url(self): - url = "https://api.workos.com/sso/jwks/test" - - client1 = _get_jwks_client(url) - client2 = _get_jwks_client(url) - - # Should be the exact same instance - assert client1 is client2 - assert id(client1) == id(client2) - - def test_jwks_client_caching_different_urls(self): - url1 = "https://api.workos.com/sso/jwks/client1" - url2 = "https://api.workos.com/sso/jwks/client2" - - client1 = _get_jwks_client(url1) - client2 = _get_jwks_client(url2) - - # Should be different instances - assert client1 is not client2 - assert id(client1) != id(client2) - - def test_jwks_cache_thread_safety(self): - url = "https://api.workos.com/sso/jwks/thread_test" - clients = [] - - def get_client(): - return _get_jwks_client(url) - - with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: - futures = [executor.submit(get_client) for _ in range(10)] - clients = [future.result() for future in futures] - - first_client = clients[0] - for client in clients[1:]: - assert client is first_client, ( - "All concurrent calls should return the same instance" - ) diff --git a/tests/test_sso.py b/tests/test_sso.py deleted file mode 100644 index 6de4450f..00000000 --- a/tests/test_sso.py +++ /dev/null @@ -1,403 +0,0 @@ -import json -from typing import Union -from urllib.parse import parse_qsl, urlparse -import pytest -from tests.types.test_auto_pagination_function import TestAutoPaginationFunction -from tests.utils.fixtures.mock_profile import MockProfile -from tests.utils.list_resource import list_data_to_dicts, list_response_of -from tests.utils.fixtures.mock_connection import MockConnection -from tests.utils.syncify import syncify -from workos.sso import SSO, AsyncSSO, SsoProviderType -from workos.types.sso import Profile -from workos.utils.request_helper import RESPONSE_TYPE_CODE - - -class SSOFixtures: - @pytest.fixture - def mock_profile(self): - return MockProfile("prof_01DWAS7ZQWM70PV93BFV1V78QV").dict() - - @pytest.fixture - def mock_magic_link_profile(self): - return Profile( - object="profile", - id="prof_01DWAS7ZQWM70PV93BFV1V78QV", - email="demo@workos-magic-link.com", - organization_id=None, - connection_id="conn_01EMH8WAK20T42N2NBMNBCYHAG", - connection_type="MagicLink", - idp_id="", - first_name=None, - last_name=None, - role=None, - roles=None, - groups=None, - raw_attributes={}, - ).dict() - - @pytest.fixture - def mock_connection(self): - return MockConnection("conn_01E4ZCR3C56J083X43JQXF3JK5").dict() - - @pytest.fixture - def mock_connection_updated(self): - connection = MockConnection("conn_01FHT48Z8J8295GZNQ4ZP1J81T").dict() - - connection["options"] = { - "signing_cert": "signing_cert", - } - - return connection - - @pytest.fixture - def mock_connections(self): - connection_list = [MockConnection(id=str(i)).dict() for i in range(10)] - - return list_response_of(data=connection_list) - - @pytest.fixture - def mock_connections_multiple_data_pages(self): - return [MockConnection(id=str(i)).dict() for i in range(40)] - - -class TestSSOBase(SSOFixtures): - provider: SsoProviderType - - @pytest.fixture(autouse=True) - def setup(self, sync_client_configuration_and_http_client_for_test): - client_configuration, http_client = ( - sync_client_configuration_and_http_client_for_test - ) - self.http_client = http_client - self.sso = SSO( - http_client=self.http_client, client_configuration=client_configuration - ) - self.provider = "GoogleOAuth" - self.customer_domain = "workos.com" - self.login_hint = "foo@workos.com" - self.redirect_uri = "https://localhost/auth/callback" - self.authorization_state = json.dumps({"things": "with_stuff"}) - self.connection_id = "connection_123" - self.organization_id = "organization_123" - self.setup_completed = True - - def test_authorization_url_throws_value_error_with_missing_connection_organization_and_provider( - self, - ): - with pytest.raises(ValueError, match=r"Incomplete arguments.*"): - self.sso.get_authorization_url( - redirect_uri=self.redirect_uri, state=self.authorization_state - ) - - def test_authorization_url_has_expected_query_params_with_provider(self): - authorization_url = self.sso.get_authorization_url( - provider=self.provider, - redirect_uri=self.redirect_uri, - state=self.authorization_state, - ) - - parsed_url = urlparse(authorization_url) - - assert parsed_url.path == "/sso/authorize" - assert dict(parse_qsl(parsed_url.query)) == { - "provider": self.provider, - "client_id": self.http_client.client_id, - "redirect_uri": self.redirect_uri, - "response_type": RESPONSE_TYPE_CODE, - "state": self.authorization_state, - } - - def test_authorization_url_has_expected_query_params_with_domain_hint(self): - authorization_url = self.sso.get_authorization_url( - connection_id=self.connection_id, - domain_hint=self.customer_domain, - redirect_uri=self.redirect_uri, - state=self.authorization_state, - ) - - parsed_url = urlparse(authorization_url) - - assert parsed_url.path == "/sso/authorize" - assert dict(parse_qsl(parsed_url.query)) == { - "domain_hint": self.customer_domain, - "client_id": self.http_client.client_id, - "redirect_uri": self.redirect_uri, - "connection": self.connection_id, - "response_type": RESPONSE_TYPE_CODE, - "state": self.authorization_state, - } - - def test_authorization_url_has_expected_query_params_with_login_hint(self): - authorization_url = self.sso.get_authorization_url( - connection_id=self.connection_id, - login_hint=self.login_hint, - redirect_uri=self.redirect_uri, - state=self.authorization_state, - ) - - parsed_url = urlparse(authorization_url) - - assert parsed_url.path == "/sso/authorize" - assert dict(parse_qsl(parsed_url.query)) == { - "login_hint": self.login_hint, - "client_id": self.http_client.client_id, - "redirect_uri": self.redirect_uri, - "connection": self.connection_id, - "response_type": RESPONSE_TYPE_CODE, - "state": self.authorization_state, - } - - def test_authorization_url_has_expected_query_params_with_connection(self): - authorization_url = self.sso.get_authorization_url( - connection_id=self.connection_id, - redirect_uri=self.redirect_uri, - state=self.authorization_state, - ) - - parsed_url = urlparse(authorization_url) - - assert parsed_url.path == "/sso/authorize" - assert dict(parse_qsl(parsed_url.query)) == { - "connection": self.connection_id, - "client_id": self.http_client.client_id, - "redirect_uri": self.redirect_uri, - "response_type": RESPONSE_TYPE_CODE, - "state": self.authorization_state, - } - - def test_authorization_url_with_string_provider_has_expected_query_params_with_organization( - self, - ): - authorization_url = self.sso.get_authorization_url( - provider=self.provider, - organization_id=self.organization_id, - redirect_uri=self.redirect_uri, - state=self.authorization_state, - ) - - parsed_url = urlparse(authorization_url) - - assert parsed_url.path == "/sso/authorize" - assert dict(parse_qsl(parsed_url.query)) == { - "organization": self.organization_id, - "provider": self.provider, - "client_id": self.http_client.client_id, - "redirect_uri": self.redirect_uri, - "response_type": RESPONSE_TYPE_CODE, - "state": self.authorization_state, - } - - def test_authorization_url_has_expected_query_params_with_organization(self): - authorization_url = self.sso.get_authorization_url( - organization_id=self.organization_id, - redirect_uri=self.redirect_uri, - state=self.authorization_state, - ) - - parsed_url = urlparse(authorization_url) - - assert parsed_url.path == "/sso/authorize" - assert dict(parse_qsl(parsed_url.query)) == { - "organization": self.organization_id, - "client_id": self.http_client.client_id, - "redirect_uri": self.redirect_uri, - "response_type": RESPONSE_TYPE_CODE, - "state": self.authorization_state, - } - - def test_authorization_url_has_expected_query_params_with_organization_and_provider( - self, - ): - authorization_url = self.sso.get_authorization_url( - organization_id=self.organization_id, - provider=self.provider, - redirect_uri=self.redirect_uri, - state=self.authorization_state, - ) - - parsed_url = urlparse(authorization_url) - - assert parsed_url.path == "/sso/authorize" - assert dict(parse_qsl(parsed_url.query)) == { - "organization": self.organization_id, - "provider": self.provider, - "client_id": self.http_client.client_id, - "redirect_uri": self.redirect_uri, - "response_type": RESPONSE_TYPE_CODE, - "state": self.authorization_state, - } - - -@pytest.mark.sync_and_async(SSO, AsyncSSO) -class TestSSO(SSOFixtures): - provider: SsoProviderType - - @pytest.fixture(autouse=True) - def setup(self, module_instance: Union[SSO, AsyncSSO]): - self.http_client = module_instance._http_client - self.sso = module_instance - self.provider = "GoogleOAuth" - self.customer_domain = "workos.com" - self.login_hint = "foo@workos.com" - self.redirect_uri = "https://localhost/auth/callback" - self.state = json.dumps({"things": "with_stuff"}) - self.connection_id = "connection_123" - self.organization_id = "organization_123" - self.setup_completed = True - - def test_get_profile_and_token_returns_expected_profile_object( - self, mock_profile, capture_and_mock_http_client_request - ): - response_dict = { - "profile": mock_profile, - "access_token": "01DY34ACQTM3B1CSX1YSZ8Z00D", - } - - request_kwargs = capture_and_mock_http_client_request( - self.http_client, response_dict, 200 - ) - - profile_and_token = syncify(self.sso.get_profile_and_token("123")) - - assert profile_and_token.access_token == "01DY34ACQTM3B1CSX1YSZ8Z00D" - assert profile_and_token.profile.dict() == mock_profile - assert request_kwargs["url"].endswith("/sso/token") - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == { - "client_id": "client_b27needthisforssotemxo", - "client_secret": "sk_test", - "code": "123", - "grant_type": "authorization_code", - } - - def test_get_profile_and_token_without_first_name_or_last_name_returns_expected_profile_object( - self, mock_magic_link_profile, capture_and_mock_http_client_request - ): - response_dict = { - "profile": mock_magic_link_profile, - "access_token": "01DY34ACQTM3B1CSX1YSZ8Z00D", - } - - request_kwargs = capture_and_mock_http_client_request( - self.http_client, response_dict, 200 - ) - - profile_and_token = syncify(self.sso.get_profile_and_token("123")) - - assert profile_and_token.access_token == "01DY34ACQTM3B1CSX1YSZ8Z00D" - assert profile_and_token.profile.dict() == mock_magic_link_profile - assert request_kwargs["url"].endswith("/sso/token") - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == { - "client_id": "client_b27needthisforssotemxo", - "client_secret": "sk_test", - "code": "123", - "grant_type": "authorization_code", - } - - def test_get_profile(self, mock_profile, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_profile, 200 - ) - - profile = syncify(self.sso.get_profile("123")) - - assert profile.dict() == mock_profile - assert request_kwargs["url"].endswith("/sso/profile") - assert request_kwargs["method"] == "get" - assert request_kwargs["headers"]["authorization"] == "Bearer 123" - - def test_get_connection( - self, mock_connection, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_connection, 200 - ) - - connection = syncify(self.sso.get_connection(connection_id="connection_id")) - - assert connection.dict() == mock_connection - assert request_kwargs["url"].endswith("/connections/connection_id") - assert request_kwargs["method"] == "get" - - def test_list_connections( - self, mock_connections, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_connections, 200 - ) - - connections = syncify(self.sso.list_connections()) - - assert list_data_to_dicts(connections.data) == mock_connections["data"] - assert request_kwargs["url"].endswith("/connections") - assert request_kwargs["method"] == "get" - - def test_list_connections_with_connection_type( - self, mock_connections, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - http_client=self.http_client, - response_dict=mock_connections, - status_code=200, - ) - - syncify(self.sso.list_connections(connection_type="GenericSAML")) - - assert request_kwargs["params"] == { - "connection_type": "GenericSAML", - "limit": 10, - "order": "desc", - } - - def test_update_connection( - self, mock_connection_updated, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_connection_updated, 200 - ) - - updated_connection = syncify( - self.sso.update_connection( - connection_id="conn_01EHT88Z8J8795GZNQ4ZP1J81T", - saml_options_signing_key="signing_key", - saml_options_signing_cert="signing_cert", - ) - ) - - assert request_kwargs["url"].endswith( - "/connections/conn_01EHT88Z8J8795GZNQ4ZP1J81T" - ) - - assert request_kwargs["method"] == "put" - assert request_kwargs["json"] == { - "options": {"signing_key": "signing_key", "signing_cert": "signing_cert"} - } - assert updated_connection.id == "conn_01FHT48Z8J8295GZNQ4ZP1J81T" - assert updated_connection.name == "Foo Corporation" - assert updated_connection.options is not None - assert updated_connection.options.signing_cert == "signing_cert" - - def test_delete_connection(self, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, - status_code=204, - headers={"content-type": "text/plain; charset=utf-8"}, - ) - - response = syncify(self.sso.delete_connection(connection_id="connection_id")) - - assert request_kwargs["url"].endswith("/connections/connection_id") - assert request_kwargs["method"] == "delete" - assert response is None - - def test_list_connections_auto_pagination( - self, - mock_connections_multiple_data_pages, - test_auto_pagination: TestAutoPaginationFunction, - ): - test_auto_pagination( - http_client=self.http_client, - list_function=self.sso.list_connections, - expected_all_page_data=mock_connections_multiple_data_pages, - ) diff --git a/tests/test_sync_http_client.py b/tests/test_sync_http_client.py deleted file mode 100644 index 9023ce15..00000000 --- a/tests/test_sync_http_client.py +++ /dev/null @@ -1,407 +0,0 @@ -from platform import python_version -from unittest.mock import MagicMock - -import httpx -import pytest - -from workos.exceptions import ( - AuthenticationException, - AuthorizationException, - BadRequestException, - BaseRequestException, - ConflictException, - EmailVerificationRequiredException, - ServerException, -) -from workos.utils.http_client import SyncHTTPClient - -STATUS_CODE_TO_EXCEPTION_MAPPING = [ - (400, BadRequestException), - (401, AuthenticationException), - (403, AuthorizationException), - (500, ServerException), -] - - -class TestSyncHTTPClient(object): - @pytest.fixture(autouse=True) - def setup(self): - response = httpx.Response(200, json={"message": "Success!"}) - - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(200, json={"message": "Success!"}) - - self.http_client = SyncHTTPClient( - api_key="sk_test", - base_url="https://api.workos.test/", - client_id="client_b27needthisforssotemxo", - version="test", - transport=httpx.MockTransport(handler), - ) - - self.http_client._client.request = MagicMock( - return_value=response, - ) - - @pytest.mark.parametrize( - "method,status_code,expected_response", - [ - ("GET", 200, {"message": "Success!"}), - ("DELETE", 204, None), - ("DELETE", 202, None), - ], - ) - def test_request_without_body( - self, method: str, status_code: int, expected_response: dict - ): - self.http_client._client.request = MagicMock( - return_value=httpx.Response( - status_code=status_code, json=expected_response - ), - ) - - response = self.http_client.request( - "events", - method=method, - params={"test_param": "test_value"}, - ) - - self.http_client._client.request.assert_called_with( - method=method, - url="https://api.workos.test/events", - headers=httpx.Headers( - { - "accept": "application/json", - "content-type": "application/json", - "user-agent": f"WorkOS Python/{python_version()} Python SDK/test", - "authorization": "Bearer sk_test", - } - ), - params={"test_param": "test_value"}, - timeout=25, - ) - - assert response == expected_response - - @pytest.mark.parametrize( - "method,status_code,expected_response", - [ - ("POST", 201, {"message": "Success!"}), - ("PUT", 200, {"message": "Success!"}), - ("PATCH", 200, {"message": "Success!"}), - ], - ) - def test_request_with_body( - self, method: str, status_code: int, expected_response: dict - ): - self.http_client._client.request = MagicMock( - return_value=httpx.Response( - status_code=status_code, json=expected_response - ), - ) - - response = self.http_client.request( - "events", method=method, json={"test_param": "test_value"} - ) - - self.http_client._client.request.assert_called_with( - method=method, - url="https://api.workos.test/events", - headers=httpx.Headers( - { - "accept": "application/json", - "content-type": "application/json", - "user-agent": f"WorkOS Python/{python_version()} Python SDK/test", - "authorization": "Bearer sk_test", - } - ), - params=None, - json={"test_param": "test_value"}, - timeout=25, - ) - - assert response == expected_response - - @pytest.mark.parametrize( - "method,status_code,expected_response", - [ - ("POST", 201, {"message": "Success!"}), - ("PUT", 200, {"message": "Success!"}), - ("PATCH", 200, {"message": "Success!"}), - ], - ) - def test_request_with_body_and_query_parameters( - self, method: str, status_code: int, expected_response: dict - ): - self.http_client._client.request = MagicMock( - return_value=httpx.Response( - status_code=status_code, json=expected_response - ), - ) - - response = self.http_client.request( - "events", - method=method, - params={"test_param": "test_param_value"}, - json={"test_json": "test_json_value"}, - ) - - self.http_client._client.request.assert_called_with( - method=method, - url="https://api.workos.test/events", - headers=httpx.Headers( - { - "accept": "application/json", - "content-type": "application/json", - "user-agent": f"WorkOS Python/{python_version()} Python SDK/test", - "authorization": "Bearer sk_test", - } - ), - params={"test_param": "test_param_value"}, - json={"test_json": "test_json_value"}, - timeout=25, - ) - - assert response == expected_response - - @pytest.mark.parametrize( - "status_code,expected_exception", - STATUS_CODE_TO_EXCEPTION_MAPPING, - ) - def test_request_raises_expected_exception_for_status_code( - self, status_code: int, expected_exception: BaseRequestException - ): - self.http_client._client.request = MagicMock( - return_value=httpx.Response(status_code=status_code), - ) - - with pytest.raises(expected_exception): # type: ignore - self.http_client.request("bad_place") - - @pytest.mark.parametrize( - "status_code,expected_exception", - STATUS_CODE_TO_EXCEPTION_MAPPING, - ) - def test_request_exceptions_include_expected_request_data( - self, status_code: int, expected_exception: BaseRequestException - ): - request_id = "request-123" - response_message = "stuff happened" - - self.http_client._client.request = MagicMock( - return_value=httpx.Response( - status_code=status_code, - json={"message": response_message}, - headers={"X-Request-ID": request_id}, - ), - ) - - try: - self.http_client.request("bad_place") - except expected_exception as ex: # type: ignore - assert ex.message == response_message - assert ex.request_id == request_id - assert ex.__class__ == expected_exception - - def test_bad_request_exceptions_include_request_data(self): - request_id = "request-123" - error = "example_error" - error_description = "Example error description" - - self.http_client._client.request = MagicMock( - return_value=httpx.Response( - status_code=400, - json={ - "error": error, - "error_description": error_description, - "foo": "bar", - }, - headers={"X-Request-ID": request_id}, - ), - ) - - try: - self.http_client.request("bad_place") - except BadRequestException as ex: - assert ( - str(ex) - == "(message=No message, request_id=request-123, error=example_error, error_description=Example error description, foo=bar)" - ) - assert ex.__class__ == BadRequestException - - def test_request_bad_body_raises_expected_exception_with_request_data(self): - request_id = "request-123" - - self.http_client._client.request = MagicMock( - return_value=httpx.Response( - status_code=200, - content="this_isnt_json", - headers={"X-Request-ID": request_id}, - ), - ) - - try: - self.http_client.request("bad_place") - except ServerException as ex: - assert ex.message is None - assert ex.request_id == request_id - assert ex.__class__ == ServerException - - def test_conflict_exception(self): - request_id = "request-123" - - self.http_client._client.request = MagicMock( - return_value=httpx.Response( - status_code=409, - headers={"X-Request-ID": request_id}, - ), - ) - - try: - self.http_client.request("bad_place") - except ConflictException as ex: - assert str(ex) == "(message=No message, request_id=request-123)" - assert ex.__class__ == ConflictException - - def test_email_verification_required_exception(self): - request_id = "request-123" - email_verification_id = "email_verification_01J6K4PMSWQXVFGF5ZQJXC6VC8" - - self.http_client._client.request = MagicMock( - return_value=httpx.Response( - status_code=403, - json={ - "message": "Please verify your email to authenticate via password.", - "code": "email_verification_required", - "email_verification_id": email_verification_id, - }, - headers={"X-Request-ID": request_id}, - ), - ) - - try: - self.http_client.request("bad_place") - except EmailVerificationRequiredException as ex: - assert ( - ex.message == "Please verify your email to authenticate via password." - ) - assert ex.code == "email_verification_required" - assert ex.email_verification_id == email_verification_id - assert ex.request_id == request_id - assert ex.__class__ == EmailVerificationRequiredException - assert isinstance(ex, AuthorizationException) - - def test_regular_authorization_exception_still_raised(self): - request_id = "request-123" - - self.http_client._client.request = MagicMock( - return_value=httpx.Response( - status_code=403, - json={ - "message": "You do not have permission to access this resource.", - "code": "forbidden", - }, - headers={"X-Request-ID": request_id}, - ), - ) - - try: - self.http_client.request("bad_place") - except AuthorizationException as ex: - assert ex.message == "You do not have permission to access this resource." - assert ex.code == "forbidden" - assert ex.request_id == request_id - assert ex.__class__ == AuthorizationException - assert not isinstance(ex, EmailVerificationRequiredException) - - def test_request_includes_base_headers(self, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 200) - - self.http_client.request("ok_place") - - default_headers = set( - (header[0].lower(), header[1]) - for header in self.http_client.default_headers.items() - ) - headers = set(request_kwargs["headers"].items()) - - assert default_headers.issubset(headers) - - def test_request_parses_json_when_content_type_present(self): - self.http_client._client.request = MagicMock( - return_value=httpx.Response( - status_code=200, - json={"foo": "bar"}, - headers={"content-type": "application/json"}, - ), - ) - - assert self.http_client.request("ok_place") == {"foo": "bar"} - - def test_request_parses_json_when_encoding_in_content_type(self): - self.http_client._client.request = MagicMock( - return_value=httpx.Response( - status_code=200, - json={"foo": "bar"}, - headers={"content-type": "application/json; charset=utf8"}, - ), - ) - - assert self.http_client.request("ok_place") == {"foo": "bar"} - - def test_request_removes_none_parameter_values( - self, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 200) - - self.http_client.request( - path="/test", - method="get", - params={"organization_id": None, "test": "value"}, - ) - assert request_kwargs["params"] == {"test": "value"} - - def test_request_removes_none_json_values( - self, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 200) - - self.http_client.request( - path="/test", - method="post", - json={"organization_id": None, "test": "value"}, - ) - assert request_kwargs["json"] == {"test": "value"} - - def test_delete_with_body_sends_json(self, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 200) - - self.http_client.delete_with_body( - path="/test", - json={"obj": "json"}, - ) - - assert request_kwargs["method"] == "delete" - assert request_kwargs["json"] == {"obj": "json"} - - def test_delete_with_body_sends_params(self, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 200) - - self.http_client.delete_with_body( - path="/test", - json={"obj1": "json"}, - params={"obj2": "params"}, - ) - - assert request_kwargs["json"] == {"obj1": "json"} - assert request_kwargs["params"] == {"obj2": "params"} - - def test_delete_without_body_raises_value_error(self): - with pytest.raises( - ValueError, match="Cannot send a body with a delete request" - ): - self.http_client.request( - path="/test", - method="delete", - json={"should": "fail"}, - ) diff --git a/tests/test_user_management.py b/tests/test_user_management.py deleted file mode 100644 index 13763690..00000000 --- a/tests/test_user_management.py +++ /dev/null @@ -1,1324 +0,0 @@ -import json -from typing import Any, Union - -from urllib.parse import parse_qsl, urlparse -import pytest - -from tests.utils.fixtures.mock_auth_factor_totp import MockAuthenticationFactorTotp -from tests.utils.fixtures.mock_email_verification import MockEmailVerification -from tests.utils.fixtures.mock_feature_flag import MockFeatureFlag -from tests.utils.fixtures.mock_invitation import MockInvitation -from tests.utils.fixtures.mock_magic_auth import MockMagicAuth -from tests.utils.fixtures.mock_organization_membership import MockOrganizationMembership -from tests.utils.fixtures.mock_password_reset import MockPasswordReset -from tests.utils.fixtures.mock_user import MockUser -from tests.utils.list_resource import list_response_of -from tests.utils.syncify import syncify -from tests.types.test_auto_pagination_function import TestAutoPaginationFunction -from workos.types.user_management.invitation import Invitation -from workos.user_management import AsyncUserManagement, UserManagement -from workos.utils.request_helper import RESPONSE_TYPE_CODE - - -class UserManagementFixtures: - @pytest.fixture - def mock_user(self): - return MockUser("user_01H7ZGXFP5C6BBQY6Z7277ZCT0").dict() - - @pytest.fixture - def mock_users_multiple_pages(self): - users_list = [MockUser(id=str(i)).dict() for i in range(40)] - return list_response_of(data=users_list) - - @pytest.fixture - def mock_organization_membership(self): - return MockOrganizationMembership("om_ABCDE").dict() - - @pytest.fixture - def mock_organization_memberships_multiple_pages(self): - organization_memberships_list = [ - MockOrganizationMembership(id=str(i)).dict() for i in range(40) - ] - return list_response_of(data=organization_memberships_list) - - @pytest.fixture - def mock_auth_response(self): - user = MockUser("user_01H7ZGXFP5C6BBQY6Z7277ZCT0").dict() - - return { - "user": user, - "organization_id": "org_12345", - "access_token": "access_token_12345", - "refresh_token": "refresh_token_12345", - } - - @pytest.fixture - def base_authentication_params(self): - return { - "client_id": "client_b27needthisforssotemxo", - "client_secret": "sk_test", - } - - @pytest.fixture - def mock_auth_refresh_token_response(self): - user = MockUser("user_01H7ZGXFP5C6BBQY6Z7277ZCT0").dict() - - return { - "access_token": "access_token_12345", - "refresh_token": "refresh_token_12345", - "user": user, - } - - @pytest.fixture - def mock_auth_response_with_impersonator(self): - user = MockUser("user_01H7ZGXFP5C6BBQY6Z7277ZCT0").dict() - - return { - "user": user, - "access_token": "access_token_12345", - "refresh_token": "refresh_token_12345", - "organization_id": "org_12345", - "impersonator": { - "email": "admin@foocorp.com", - "reason": "Debugging an account issue.", - }, - } - - @pytest.fixture - def mock_magic_auth_challenge_response(self): - return { - "id": "auth_challenge_01E4ZCR3C56J083X43JQXF3JK5", - } - - @pytest.fixture - def mock_enroll_auth_factor_response(self): - return { - "authentication_factor": { - "object": "authentication_factor", - "id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", - "user_id": "user_12345", - "created_at": "2022-02-15T15:14:19.392Z", - "updated_at": "2022-02-15T15:14:19.392Z", - "type": "totp", - "totp": { - "issuer": "FooCorp", - "user": "test@example.com", - "qr_code": "data:image/png;base64,{base64EncodedPng}", - "secret": "NAGCCFS3EYRB422HNAKAKY3XDUORMSRF", - "uri": "otpauth://totp/FooCorp:alan.turing@foo-corp.com?secret=NAGCCFS3EYRB422HNAKAKY3XDUORMSRF&issuer=FooCorp", - }, - }, - "authentication_challenge": { - "object": "authentication_challenge", - "id": "auth_challenge_01FVYZWQTZQ5VB6BC5MPG2EYC5", - "created_at": "2022-02-15T15:26:53.274Z", - "updated_at": "2022-02-15T15:26:53.274Z", - "expires_at": "2022-02-15T15:36:53.279Z", - "code": None, - "authentication_factor_id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", - }, - } - - @pytest.fixture - def mock_auth_factors_multiple_pages(self): - auth_factors_list = [ - MockAuthenticationFactorTotp(id=str(i)).dict() for i in range(40) - ] - return list_response_of(data=auth_factors_list) - - @pytest.fixture - def mock_email_verification(self): - return MockEmailVerification("email_verification_ABCDE").dict() - - @pytest.fixture - def mock_magic_auth(self): - return MockMagicAuth("magic_auth_ABCDE").dict() - - @pytest.fixture - def mock_password_reset(self): - return MockPasswordReset("password_reset_ABCDE").dict() - - @pytest.fixture - def mock_invitation(self): - return MockInvitation("invitation_ABCDE").dict() - - @pytest.fixture - def mock_invitations_multiple_pages(self): - invitations_list = [MockInvitation(id=str(i)).dict() for i in range(40)] - return list_response_of(data=invitations_list) - - @pytest.fixture - def mock_feature_flags(self): - return { - "data": [MockFeatureFlag(id=f"flag_{str(i)}").dict() for i in range(2)], - "object": "list", - "list_metadata": {"before": None, "after": None}, - } - - -class TestUserManagementBase(UserManagementFixtures): - @pytest.fixture(autouse=True) - def setup(self, sync_client_configuration_and_http_client_for_test): - ( - client_configuration, - http_client, - ) = sync_client_configuration_and_http_client_for_test - self.http_client = http_client - self.user_management = UserManagement( - http_client=self.http_client, client_configuration=client_configuration - ) - - def test_authorization_url_throws_value_error_with_missing_connection_organization_and_provider( - self, - ): - redirect_uri = "https://localhost/auth/callback" - with pytest.raises(ValueError, match=r"Incomplete arguments.*"): - self.user_management.get_authorization_url(redirect_uri=redirect_uri) - - def test_authorization_url_has_expected_query_params_with_connection_id(self): - connection_id = "connection_123" - redirect_uri = "https://localhost/auth/callback" - authorization_url = self.user_management.get_authorization_url( - connection_id=connection_id, - redirect_uri=redirect_uri, - ) - - parsed_url = urlparse(authorization_url) - assert parsed_url.path == "/user_management/authorize" - assert dict(parse_qsl(str(parsed_url.query))) == { - "connection_id": connection_id, - "client_id": self.http_client.client_id, - "redirect_uri": redirect_uri, - "response_type": RESPONSE_TYPE_CODE, - } - - def test_authorization_url_has_expected_query_params_with_organization_id(self): - organization_id = "organization_123" - redirect_uri = "https://localhost/auth/callback" - authorization_url = self.user_management.get_authorization_url( - organization_id=organization_id, - redirect_uri=redirect_uri, - ) - - parsed_url = urlparse(authorization_url) - assert parsed_url.path == "/user_management/authorize" - assert dict(parse_qsl(str(parsed_url.query))) == { - "organization_id": organization_id, - "client_id": self.http_client.client_id, - "redirect_uri": redirect_uri, - "response_type": RESPONSE_TYPE_CODE, - } - - def test_authorization_url_has_expected_query_params_with_provider(self): - provider = "GoogleOAuth" - redirect_uri = "https://localhost/auth/callback" - authorization_url = self.user_management.get_authorization_url( - provider=provider, redirect_uri=redirect_uri - ) - - parsed_url = urlparse(authorization_url) - assert parsed_url.path == "/user_management/authorize" - assert dict(parse_qsl(str(parsed_url.query))) == { - "provider": provider, - "client_id": self.http_client.client_id, - "redirect_uri": redirect_uri, - "response_type": RESPONSE_TYPE_CODE, - } - - def test_authorization_url_has_expected_query_params_with_prompt(self): - provider = "GoogleOAuth" - redirect_uri = "https://localhost/auth/callback" - prompt = "consent" - authorization_url = self.user_management.get_authorization_url( - provider=provider, - redirect_uri=redirect_uri, - prompt=prompt, - ) - - parsed_url = urlparse(authorization_url) - assert parsed_url.path == "/user_management/authorize" - assert dict(parse_qsl(str(parsed_url.query))) == { - "client_id": self.http_client.client_id, - "redirect_uri": redirect_uri, - "response_type": RESPONSE_TYPE_CODE, - "provider": provider, - "prompt": prompt, - } - - def test_authorization_url_has_expected_query_params_with_domain_hint(self): - connection_id = "connection_123" - redirect_uri = "https://localhost/auth/callback" - domain_hint = "workos.com" - - authorization_url = self.user_management.get_authorization_url( - connection_id=connection_id, - domain_hint=domain_hint, - redirect_uri=redirect_uri, - ) - - parsed_url = urlparse(authorization_url) - assert parsed_url.path == "/user_management/authorize" - assert dict(parse_qsl(str(parsed_url.query))) == { - "domain_hint": domain_hint, - "client_id": self.http_client.client_id, - "redirect_uri": redirect_uri, - "connection_id": connection_id, - "response_type": RESPONSE_TYPE_CODE, - } - - def test_authorization_url_has_expected_query_params_with_login_hint(self): - connection_id = "connection_123" - redirect_uri = "https://localhost/auth/callback" - login_hint = "foo@workos.com" - - authorization_url = self.user_management.get_authorization_url( - connection_id=connection_id, - login_hint=login_hint, - redirect_uri=redirect_uri, - ) - - parsed_url = urlparse(authorization_url) - assert parsed_url.path == "/user_management/authorize" - assert dict(parse_qsl(str(parsed_url.query))) == { - "login_hint": login_hint, - "client_id": self.http_client.client_id, - "redirect_uri": redirect_uri, - "connection_id": connection_id, - "response_type": RESPONSE_TYPE_CODE, - } - - def test_authorization_url_has_expected_query_params_with_state(self): - connection_id = "connection_123" - redirect_uri = "https://localhost/auth/callback" - state = json.dumps({"things": "with_stuff"}) - - authorization_url = self.user_management.get_authorization_url( - connection_id=connection_id, - state=state, - redirect_uri=redirect_uri, - ) - - parsed_url = urlparse(authorization_url) - assert parsed_url.path == "/user_management/authorize" - assert dict(parse_qsl(str(parsed_url.query))) == { - "state": state, - "client_id": self.http_client.client_id, - "redirect_uri": redirect_uri, - "connection_id": connection_id, - "response_type": RESPONSE_TYPE_CODE, - } - - def test_authorization_url_has_expected_query_params_with_code_challenge(self): - connection_id = "connection_123" - redirect_uri = "https://localhost/auth/callback" - code_challenge = json.dumps({"code_challenge": "code_challenge_for_pkce"}) - - authorization_url = self.user_management.get_authorization_url( - connection_id=connection_id, - code_challenge=code_challenge, - redirect_uri=redirect_uri, - ) - - parsed_url = urlparse(authorization_url) - assert parsed_url.path == "/user_management/authorize" - assert dict(parse_qsl(str(parsed_url.query))) == { - "code_challenge": code_challenge, - "code_challenge_method": "S256", - "client_id": self.http_client.client_id, - "redirect_uri": redirect_uri, - "connection_id": connection_id, - "response_type": RESPONSE_TYPE_CODE, - } - - def test_authorization_url_has_expected_query_params_with_screen_hint(self): - connection_id = "connection_123" - redirect_uri = "https://localhost/auth/callback" - screen_hint = "sign-up" - - authorization_url = self.user_management.get_authorization_url( - connection_id=connection_id, - screen_hint=screen_hint, - redirect_uri=redirect_uri, - provider="authkit", - ) - - parsed_url = urlparse(authorization_url) - assert parsed_url.path == "/user_management/authorize" - assert dict(parse_qsl(str(parsed_url.query))) == { - "screen_hint": screen_hint, - "client_id": self.http_client.client_id, - "redirect_uri": redirect_uri, - "connection_id": connection_id, - "response_type": RESPONSE_TYPE_CODE, - "provider": "authkit", - } - - def test_authorization_url_has_expected_query_params_with_provider_scopes(self): - provider = "GoogleOAuth" - provider_scopes = [ - "https://www.googleapis.com/auth/calendar", - "https://www.googleapis.com/auth/admin.directory.group", - ] - redirect_uri = "https://localhost/auth/callback" - authorization_url = self.user_management.get_authorization_url( - provider=provider, - provider_scopes=provider_scopes, - redirect_uri=redirect_uri, - ) - - parsed_url = urlparse(authorization_url) - assert parsed_url.path == "/user_management/authorize" - assert dict(parse_qsl(str(parsed_url.query))) == { - "provider": provider, - "provider_scopes": ",".join(provider_scopes), - "client_id": self.http_client.client_id, - "redirect_uri": redirect_uri, - "response_type": RESPONSE_TYPE_CODE, - } - - def test_get_jwks_url(self): - expected = "%ssso/jwks/%s" % ( - self.http_client.base_url, - self.http_client.client_id, - ) - result = self.user_management.get_jwks_url() - - assert expected == result - - def test_get_logout_url(self): - expected = "%suser_management/sessions/logout?session_id=%s" % ( - self.http_client.base_url, - "session_123", - ) - result = self.user_management.get_logout_url("session_123") - - assert expected == result - - def test_get_logout_url_with_return_to(self): - expected = "https://api.workos.test/user_management/sessions/logout?session_id=session_123&return_to=https%3A%2F%2Fexample.com%2Fsigned-out" - result = self.user_management.get_logout_url( - "session_123", return_to="https://example.com/signed-out" - ) - - assert expected == result - - -@pytest.mark.sync_and_async(UserManagement, AsyncUserManagement) -class TestUserManagement(UserManagementFixtures): - @pytest.fixture(autouse=True) - def setup(self, module_instance: Union[UserManagement, AsyncUserManagement]): - self.http_client = module_instance._http_client - self.user_management = module_instance - - def test_get_user(self, mock_user, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_user, 200 - ) - - user_id = "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - user = syncify(self.user_management.get_user(user_id=user_id)) - - assert request_kwargs["url"].endswith(f"user_management/users/{user_id}") - assert request_kwargs["method"] == "get" - assert user.id == "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - assert user.profile_picture_url == "https://example.com/profile-picture.jpg" - assert user.last_sign_in_at == "2021-06-25T19:07:33.155Z" - assert user.locale == "en-US" - - def test_get_user_by_external_id( - self, mock_user, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_user, 200 - ) - - external_id = "external-id" - user = syncify( - self.user_management.get_user_by_external_id(external_id=external_id) - ) - - assert request_kwargs["url"].endswith( - f"user_management/users/external_id/{external_id}" - ) - assert request_kwargs["method"] == "get" - assert user.id == "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - assert user.profile_picture_url == "https://example.com/profile-picture.jpg" - assert user.last_sign_in_at == "2021-06-25T19:07:33.155Z" - assert user.locale == "en-US" - assert user.metadata == mock_user["metadata"] - - def test_list_users_auto_pagination( - self, - mock_users_multiple_pages, - test_auto_pagination: TestAutoPaginationFunction, - ): - test_auto_pagination( - http_client=self.http_client, - list_function=self.user_management.list_users, - expected_all_page_data=mock_users_multiple_pages["data"], - ) - - def test_create_user(self, mock_user, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_user, 201 - ) - - payload = { - "email": "marcelina@foo-corp.com", - "first_name": "Marcelina", - "last_name": "Hoeger", - "password": "password", - "email_verified": False, - } - user = syncify(self.user_management.create_user(**payload)) - - assert request_kwargs["url"].endswith("user_management/users") - assert request_kwargs["json"] == payload - assert user.id == "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - - def test_update_user(self, mock_user, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_user, 200 - ) - - params = { - "first_name": "Marcelina", - "last_name": "Hoeger", - "email_verified": True, - "password": "password", - } - user = syncify( - self.user_management.update_user( - user_id="user_01H7ZGXFP5C6BBQY6Z7277ZCT0", **params - ) - ) - - assert request_kwargs["url"].endswith("users/user_01H7ZGXFP5C6BBQY6Z7277ZCT0") - assert user.id == "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - assert request_kwargs["method"] == "put" - assert request_kwargs["json"] == { - "first_name": "Marcelina", - "last_name": "Hoeger", - "email_verified": True, - "password": "password", - } - - def test_update_user_with_locale( - self, mock_user, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_user, 200 - ) - - params: dict[str, Any] = { - "first_name": "Marcelina", - "locale": "fr-FR", - } - user = syncify( - self.user_management.update_user( - user_id="user_01H7ZGXFP5C6BBQY6Z7277ZCT0", **params - ) - ) - - assert request_kwargs["url"].endswith("users/user_01H7ZGXFP5C6BBQY6Z7277ZCT0") - assert user.id == "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - assert request_kwargs["method"] == "put" - assert request_kwargs["json"]["first_name"] == "Marcelina" - assert request_kwargs["json"]["locale"] == "fr-FR" - - def test_delete_user(self, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - http_client=self.http_client, status_code=204 - ) - - response = syncify( - self.user_management.delete_user("user_01H7ZGXFP5C6BBQY6Z7277ZCT0") - ) - - assert request_kwargs["url"].endswith( - "user_management/users/user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - ) - assert request_kwargs["method"] == "delete" - assert response is None - - def test_create_organization_membership( - self, capture_and_mock_http_client_request, mock_organization_membership - ): - user_id = "user_12345" - organization_id = "org_67890" - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_organization_membership, 201 - ) - - organization_membership = syncify( - self.user_management.create_organization_membership( - user_id=user_id, organization_id=organization_id - ) - ) - - assert request_kwargs["url"].endswith( - "user_management/organization_memberships" - ) - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == { - "user_id": user_id, - "organization_id": organization_id, - } - assert organization_membership.user_id == user_id - assert organization_membership.organization_id == organization_id - - def test_update_organization_membership( - self, capture_and_mock_http_client_request, mock_organization_membership - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_organization_membership, 201 - ) - - organization_membership = syncify( - self.user_management.update_organization_membership( - organization_membership_id="om_ABCDE", - role_slug="member", - ) - ) - - assert request_kwargs["url"].endswith( - "user_management/organization_memberships/om_ABCDE" - ) - assert request_kwargs["method"] == "put" - assert request_kwargs["json"] == {"role_slug": "member"} - assert organization_membership.id == "om_ABCDE" - assert organization_membership.role == {"slug": "member"} - - def test_get_organization_membership( - self, mock_organization_membership, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_organization_membership, 200 - ) - - om = syncify(self.user_management.get_organization_membership("om_ABCDE")) - - assert request_kwargs["url"].endswith( - "user_management/organization_memberships/om_ABCDE" - ) - assert request_kwargs["method"] == "get" - assert om.id == "om_ABCDE" - - def test_delete_organization_membership(self, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request( - http_client=self.http_client, status_code=200 - ) - - response = syncify( - self.user_management.delete_organization_membership("om_ABCDE") - ) - - assert request_kwargs["url"].endswith( - "user_management/organization_memberships/om_ABCDE" - ) - assert request_kwargs["method"] == "delete" - assert response is None - - def test_list_organization_memberships_auto_pagination( - self, - mock_organization_memberships_multiple_pages, - test_auto_pagination: TestAutoPaginationFunction, - ): - test_auto_pagination( - http_client=self.http_client, - list_function=self.user_management.list_organization_memberships, - expected_all_page_data=mock_organization_memberships_multiple_pages["data"], - ) - - def test_deactivate_organization_membership( - self, mock_organization_membership, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_organization_membership, 200 - ) - - om = syncify( - self.user_management.deactivate_organization_membership("om_ABCDE") - ) - - assert request_kwargs["url"].endswith( - "user_management/organization_memberships/om_ABCDE/deactivate" - ) - assert request_kwargs["method"] == "put" - assert om.id == "om_ABCDE" - - def test_reactivate_organization_membership( - self, mock_organization_membership, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_organization_membership, 200 - ) - - om = syncify( - self.user_management.reactivate_organization_membership("om_ABCDE") - ) - - assert request_kwargs["url"].endswith( - "user_management/organization_memberships/om_ABCDE/reactivate" - ) - assert request_kwargs["method"] == "put" - assert om.id == "om_ABCDE" - - def test_authenticate_with_password( - self, - capture_and_mock_http_client_request, - mock_auth_response, - base_authentication_params, - ): - params = { - "email": "marcelina@foo-corp.com", - "password": "test123", - "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", - "ip_address": "192.0.0.1", - } - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_auth_response, 200 - ) - - response = syncify(self.user_management.authenticate_with_password(**params)) - - assert request_kwargs["url"].endswith("user_management/authenticate") - assert request_kwargs["method"] == "post" - assert response.user.id == "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - assert response.organization_id == "org_12345" - assert response.access_token == "access_token_12345" - assert response.refresh_token == "refresh_token_12345" - assert request_kwargs["json"] == { - **params, - **base_authentication_params, - "grant_type": "password", - } - - def test_authenticate_with_code( - self, - capture_and_mock_http_client_request, - mock_auth_response, - base_authentication_params, - ): - params: dict[str, Any] = { - "code": "test_code", - "code_verifier": "test_code_verifier", - "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", - "ip_address": "192.0.0.1", - } - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_auth_response, 200 - ) - - response = syncify(self.user_management.authenticate_with_code(**params)) - - assert request_kwargs["url"].endswith("user_management/authenticate") - assert request_kwargs["method"] == "post" - assert response.user.id == "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - assert response.organization_id == "org_12345" - assert response.access_token == "access_token_12345" - assert response.refresh_token == "refresh_token_12345" - assert request_kwargs["json"] == { - **params, - **base_authentication_params, - "grant_type": "authorization_code", - } - - def test_authenticate_impersonator_with_code( - self, - capture_and_mock_http_client_request, - mock_auth_response_with_impersonator, - base_authentication_params, - ): - params: dict[str, Any] = {"code": "test_code"} - - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_auth_response_with_impersonator, 200 - ) - - response = syncify(self.user_management.authenticate_with_code(**params)) - - assert request_kwargs["url"].endswith("user_management/authenticate") - assert request_kwargs["method"] == "post" - assert response.user.id == "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - assert response.impersonator is not None - assert response.impersonator.dict() == { - "email": "admin@foocorp.com", - "reason": "Debugging an account issue.", - } - assert request_kwargs["json"] == { - **params, - **base_authentication_params, - "grant_type": "authorization_code", - } - - def test_authenticate_with_code_with_invitation_token( - self, - capture_and_mock_http_client_request, - mock_auth_response, - base_authentication_params, - ): - params: dict[str, Any] = { - "code": "test_code", - "code_verifier": "test_code_verifier", - "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", - "ip_address": "192.0.0.1", - "invitation_token": "invitation_token_12345", - } - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_auth_response, 200 - ) - - response = syncify(self.user_management.authenticate_with_code(**params)) - - assert request_kwargs["url"].endswith("user_management/authenticate") - assert request_kwargs["method"] == "post" - assert response.user.id == "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - assert response.organization_id == "org_12345" - assert response.access_token == "access_token_12345" - assert response.refresh_token == "refresh_token_12345" - assert request_kwargs["json"] == { - **params, - **base_authentication_params, - "grant_type": "authorization_code", - } - - def test_authenticate_with_magic_auth( - self, - capture_and_mock_http_client_request, - mock_auth_response, - base_authentication_params, - ): - params = { - "code": "test_auth", - "email": "marcelina@foo-corp.com", - "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", - "ip_address": "192.0.0.1", - } - - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_auth_response, 200 - ) - - response = syncify(self.user_management.authenticate_with_magic_auth(**params)) - - assert request_kwargs["url"].endswith("user_management/authenticate") - assert request_kwargs["method"] == "post" - assert response.user.id == "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - assert response.organization_id == "org_12345" - assert response.access_token == "access_token_12345" - assert response.refresh_token == "refresh_token_12345" - assert request_kwargs["json"] == { - **params, - **base_authentication_params, - "grant_type": "urn:workos:oauth:grant-type:magic-auth:code", - } - - def test_authenticate_with_email_verification( - self, - capture_and_mock_http_client_request, - mock_auth_response, - base_authentication_params, - ): - params = { - "code": "test_auth", - "pending_authentication_token": "ql1AJgNoLN1tb9llaQ8jyC2dn", - "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", - "ip_address": "192.0.0.1", - } - - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_auth_response, 200 - ) - - response = syncify( - self.user_management.authenticate_with_email_verification(**params) - ) - - assert request_kwargs["url"].endswith("user_management/authenticate") - assert request_kwargs["method"] == "post" - assert response.user.id == "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - assert response.organization_id == "org_12345" - assert response.access_token == "access_token_12345" - assert response.refresh_token == "refresh_token_12345" - assert request_kwargs["json"] == { - **params, - **base_authentication_params, - "grant_type": "urn:workos:oauth:grant-type:email-verification:code", - } - - def test_authenticate_with_totp( - self, - capture_and_mock_http_client_request, - mock_auth_response, - base_authentication_params, - ): - params = { - "code": "test_auth", - "authentication_challenge_id": "auth_challenge_01FVYZWQTZQ5VB6BC5MPG2EYC5", - "pending_authentication_token": "ql1AJgNoLN1tb9llaQ8jyC2dn", - "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", - "ip_address": "192.0.0.1", - } - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_auth_response, 200 - ) - - response = syncify(self.user_management.authenticate_with_totp(**params)) - - assert request_kwargs["url"].endswith("user_management/authenticate") - assert request_kwargs["method"] == "post" - assert response.user.id == "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - assert response.organization_id == "org_12345" - assert response.access_token == "access_token_12345" - assert response.refresh_token == "refresh_token_12345" - assert request_kwargs["json"] == { - **params, - **base_authentication_params, - "grant_type": "urn:workos:oauth:grant-type:mfa-totp", - } - - def test_authenticate_with_organization_selection( - self, - capture_and_mock_http_client_request, - mock_auth_response, - base_authentication_params, - ): - params = { - "organization_id": "org_12345", - "pending_authentication_token": "ql1AJgNoLN1tb9llaQ8jyC2dn", - "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", - "ip_address": "192.0.0.1", - } - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_auth_response, 200 - ) - - response = syncify( - self.user_management.authenticate_with_organization_selection(**params) - ) - - assert request_kwargs["url"].endswith("user_management/authenticate") - assert request_kwargs["method"] == "post" - assert response.user.id == "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - assert response.organization_id == "org_12345" - assert response.access_token == "access_token_12345" - assert response.refresh_token == "refresh_token_12345" - assert request_kwargs["json"] == { - **params, - **base_authentication_params, - "grant_type": "urn:workos:oauth:grant-type:organization-selection", - } - - def test_authenticate_with_refresh_token( - self, - capture_and_mock_http_client_request, - mock_auth_refresh_token_response, - base_authentication_params, - ): - params: dict[str, Any] = { - "refresh_token": "refresh_token_98765", - "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", - "ip_address": "192.0.0.1", - } - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_auth_refresh_token_response, 200 - ) - - response = syncify( - self.user_management.authenticate_with_refresh_token(**params) - ) - - assert request_kwargs["url"].endswith("user_management/authenticate") - assert request_kwargs["method"] == "post" - assert response.access_token == "access_token_12345" - assert response.refresh_token == "refresh_token_12345" - assert request_kwargs["json"] == { - **params, - **base_authentication_params, - "grant_type": "refresh_token", - } - - def test_get_password_reset( - self, mock_password_reset, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_password_reset, 200 - ) - - password_reset = syncify( - self.user_management.get_password_reset("password_reset_ABCDE") - ) - - assert request_kwargs["url"].endswith( - "user_management/password_reset/password_reset_ABCDE" - ) - assert request_kwargs["method"] == "get" - assert password_reset.id == "password_reset_ABCDE" - - def test_create_password_reset( - self, capture_and_mock_http_client_request, mock_password_reset - ): - email = "marcelina@foo-corp.com" - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_password_reset, 201 - ) - - password_reset = syncify( - self.user_management.create_password_reset(email=email) - ) - - assert request_kwargs["url"].endswith("user_management/password_reset") - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == {"email": email} - assert password_reset.email == email - - def test_reset_password(self, capture_and_mock_http_client_request, mock_user): - json = { - "token": "token123", - "new_password": "pass123", - } - request_kwargs = capture_and_mock_http_client_request( - self.http_client, {"user": mock_user}, 200 - ) - - response = syncify(self.user_management.reset_password(**json)) - - assert request_kwargs["url"].endswith("user_management/password_reset/confirm") - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == json - assert response.id == "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - - def test_get_email_verification( - self, mock_email_verification, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_email_verification, 200 - ) - - email_verification = syncify( - self.user_management.get_email_verification("email_verification_ABCDE") - ) - - assert request_kwargs["url"].endswith( - "user_management/email_verification/email_verification_ABCDE" - ) - assert request_kwargs["method"] == "get" - assert email_verification.id == "email_verification_ABCDE" - - def test_send_verification_email( - self, capture_and_mock_http_client_request, mock_user - ): - user_id = "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - - request_kwargs = capture_and_mock_http_client_request( - self.http_client, {"user": mock_user}, 200 - ) - - response = syncify( - self.user_management.send_verification_email(user_id=user_id) - ) - - assert request_kwargs["url"].endswith( - "user_management/users/user_01H7ZGXFP5C6BBQY6Z7277ZCT0/email_verification/send" - ) - assert request_kwargs["method"] == "post" - assert response.id == "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - - def test_verify_email(self, capture_and_mock_http_client_request, mock_user): - user_id = "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - code = "code_123" - - request_kwargs = capture_and_mock_http_client_request( - self.http_client, {"user": mock_user}, 200 - ) - - response = syncify( - self.user_management.verify_email(user_id=user_id, code=code) - ) - - assert request_kwargs["url"].endswith( - "user_management/users/user_01H7ZGXFP5C6BBQY6Z7277ZCT0/email_verification/confirm" - ) - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == {"code": code} - assert response.id == "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - - def test_get_magic_auth( - self, mock_magic_auth, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_magic_auth, 200 - ) - - magic_auth = syncify(self.user_management.get_magic_auth("magic_auth_ABCDE")) - - assert request_kwargs["url"].endswith( - "user_management/magic_auth/magic_auth_ABCDE" - ) - assert request_kwargs["method"] == "get" - assert magic_auth.id == "magic_auth_ABCDE" - - def test_create_magic_auth( - self, capture_and_mock_http_client_request, mock_magic_auth - ): - email = "marcelina@foo-corp.com" - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_magic_auth, 201 - ) - - magic_auth = syncify(self.user_management.create_magic_auth(email=email)) - - assert request_kwargs["url"].endswith("user_management/magic_auth") - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == {"email": email} - assert magic_auth.email == email - - def test_enroll_auth_factor( - self, mock_enroll_auth_factor_response, capture_and_mock_http_client_request - ): - user_id = "user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - type = "totp" - totp_issuer = "WorkOS" - totp_user = "marcelina@foo-corp.com" - totp_secret = "secret-test" - - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_enroll_auth_factor_response, 200 - ) - - enroll_auth_factor = syncify( - self.user_management.enroll_auth_factor( - user_id=user_id, - type=type, - totp_issuer=totp_issuer, - totp_user=totp_user, - totp_secret=totp_secret, - ) - ) - - assert request_kwargs["url"].endswith( - "user_management/users/user_01H7ZGXFP5C6BBQY6Z7277ZCT0/auth_factors" - ) - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == { - "type": "totp", - "totp_issuer": "WorkOS", - "totp_user": "marcelina@foo-corp.com", - "totp_secret": "secret-test", - } - assert enroll_auth_factor.dict() == mock_enroll_auth_factor_response - - def test_list_auth_factors_auto_pagination( - self, - mock_auth_factors_multiple_pages, - test_auto_pagination: TestAutoPaginationFunction, - ): - test_auto_pagination( - http_client=self.http_client, - list_function=self.user_management.list_auth_factors, - list_function_params={"user_id": "user_12345"}, - expected_all_page_data=mock_auth_factors_multiple_pages["data"], - url_path_keys=["user_id"], - ) - - def test_get_invitation( - self, mock_invitation, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_invitation, 200 - ) - - invitation = syncify(self.user_management.get_invitation("invitation_ABCDE")) - - assert request_kwargs["url"].endswith( - "user_management/invitations/invitation_ABCDE" - ) - assert request_kwargs["method"] == "get" - assert invitation.id == "invitation_ABCDE" - - def test_find_invitation_by_token( - self, mock_invitation, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_invitation, 200 - ) - - invitation = syncify( - self.user_management.find_invitation_by_token("Z1uX3RbwcIl5fIGJJJCXXisdI") - ) - - assert request_kwargs["url"].endswith( - "user_management/invitations/by_token/Z1uX3RbwcIl5fIGJJJCXXisdI" - ) - assert request_kwargs["method"] == "get" - assert invitation.token == "Z1uX3RbwcIl5fIGJJJCXXisdI" - - def test_list_invitations_auto_pagination( - self, - mock_invitations_multiple_pages, - test_auto_pagination: TestAutoPaginationFunction, - ): - test_auto_pagination( - http_client=self.http_client, - list_function=self.user_management.list_invitations, - list_function_params={"organization_id": "org_12345"}, - expected_all_page_data=mock_invitations_multiple_pages["data"], - ) - - def test_send_invitation( - self, capture_and_mock_http_client_request, mock_invitation - ): - email = "marcelina@foo-corp.com" - organization_id = "org_12345" - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_invitation, 201 - ) - - invitation = syncify( - self.user_management.send_invitation( - email=email, organization_id=organization_id - ) - ) - - assert request_kwargs["url"].endswith("user_management/invitations") - assert request_kwargs["json"] == { - "email": email, - "organization_id": organization_id, - } - assert invitation.email == email - assert invitation.organization_id == organization_id - - def test_revoke_invitation( - self, capture_and_mock_http_client_request, mock_invitation - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_invitation, 200 - ) - - invitation = syncify(self.user_management.revoke_invitation("invitation_ABCDE")) - - assert request_kwargs["url"].endswith( - "user_management/invitations/invitation_ABCDE/revoke" - ) - assert request_kwargs["method"] == "post" - assert isinstance(invitation, Invitation) - - def test_resend_invitation( - self, capture_and_mock_http_client_request, mock_invitation - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_invitation, 200 - ) - - invitation = syncify(self.user_management.resend_invitation("invitation_ABCDE")) - - assert request_kwargs["url"].endswith( - "user_management/invitations/invitation_ABCDE/resend" - ) - assert request_kwargs["method"] == "post" - assert isinstance(invitation, Invitation) - assert invitation.id == "invitation_ABCDE" - - def test_resend_invitation_not_found(self, capture_and_mock_http_client_request): - error_response = { - "message": "Invitation not found", - "code": "not_found", - } - capture_and_mock_http_client_request(self.http_client, error_response, 404) - - with pytest.raises(Exception): - syncify(self.user_management.resend_invitation("invitation_nonexistent")) - - def test_resend_invitation_expired(self, capture_and_mock_http_client_request): - error_response = { - "message": "Invite has expired.", - "code": "invite_expired", - } - capture_and_mock_http_client_request(self.http_client, error_response, 400) - - with pytest.raises(Exception): - syncify(self.user_management.resend_invitation("invitation_expired")) - - def test_resend_invitation_revoked(self, capture_and_mock_http_client_request): - error_response = { - "message": "Invite has been revoked.", - "code": "invite_revoked", - } - capture_and_mock_http_client_request(self.http_client, error_response, 400) - - with pytest.raises(Exception): - syncify(self.user_management.resend_invitation("invitation_revoked")) - - def test_resend_invitation_accepted(self, capture_and_mock_http_client_request): - error_response = { - "message": "Invite has already been accepted.", - "code": "invite_accepted", - } - capture_and_mock_http_client_request(self.http_client, error_response, 400) - - with pytest.raises(Exception): - syncify(self.user_management.resend_invitation("invitation_accepted")) - - def test_accept_invitation( - self, capture_and_mock_http_client_request, mock_invitation - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_invitation, 200 - ) - - invitation = syncify(self.user_management.accept_invitation("invitation_ABCDE")) - - assert request_kwargs["url"].endswith( - "user_management/invitations/invitation_ABCDE/accept" - ) - assert request_kwargs["method"] == "post" - assert isinstance(invitation, Invitation) - assert invitation.id == "invitation_ABCDE" - - def test_accept_invitation_not_found(self, capture_and_mock_http_client_request): - error_response = { - "message": "Invitation not found", - "code": "not_found", - } - capture_and_mock_http_client_request(self.http_client, error_response, 404) - - with pytest.raises(Exception): - syncify(self.user_management.accept_invitation("invitation_nonexistent")) - - def test_accept_invitation_already_accepted( - self, capture_and_mock_http_client_request - ): - error_response = { - "message": "Invite has already been accepted.", - "code": "invite_accepted", - } - capture_and_mock_http_client_request(self.http_client, error_response, 400) - - with pytest.raises(Exception): - syncify(self.user_management.accept_invitation("invitation_accepted")) - - def test_list_feature_flags( - self, mock_feature_flags, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_feature_flags, 200 - ) - - feature_flags_response = syncify( - self.user_management.list_feature_flags( - user_id="user_01H7ZGXFP5C6BBQY6Z7277ZCT0" - ) - ) - - def to_dict(x): - return x.dict() - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith( - "/user_management/users/user_01H7ZGXFP5C6BBQY6Z7277ZCT0/feature-flags" - ) - assert ( - list(map(to_dict, feature_flags_response.data)) - == mock_feature_flags["data"] - ) diff --git a/tests/test_user_management_list_sessions.py b/tests/test_user_management_list_sessions.py deleted file mode 100644 index c9d4065c..00000000 --- a/tests/test_user_management_list_sessions.py +++ /dev/null @@ -1,73 +0,0 @@ -from typing import Union - -import pytest - -from tests.utils.list_resource import list_response_of -from tests.utils.syncify import syncify -from tests.types.test_auto_pagination_function import TestAutoPaginationFunction -from workos.user_management import AsyncUserManagement, UserManagement - - -def _mock_session(id: str): - now = "2025-07-23T14:00:00.000Z" - return { - "object": "session", - "id": id, - "user_id": "user_123", - "organization_id": "org_123", - "status": "active", - "auth_method": "password", - "impersonator": None, - "ip_address": "192.168.1.1", - "user_agent": "Mozilla/5.0", - "expires_at": "2025-07-23T15:00:00.000Z", - "ended_at": None, - "created_at": now, - "updated_at": now, - } - - -@pytest.mark.sync_and_async(UserManagement, AsyncUserManagement) -class TestUserManagementListSessions: - @pytest.fixture(autouse=True) - def setup(self, module_instance: Union[UserManagement, AsyncUserManagement]): - self.http_client = module_instance._http_client - self.user_management = module_instance - - def test_list_sessions_query_and_parsing( - self, capture_and_mock_http_client_request - ): - sessions = [_mock_session("session_1"), _mock_session("session_2")] - response = list_response_of(data=sessions) - request_kwargs = capture_and_mock_http_client_request( - self.http_client, response, 200 - ) - - result = syncify( - self.user_management.list_sessions( - user_id="user_123", limit=10, before="before_id", order="desc" - ) - ) - - assert request_kwargs["url"].endswith("user_management/users/user_123/sessions") - assert request_kwargs["method"] == "get" - assert request_kwargs["params"]["limit"] == 10 - assert request_kwargs["params"]["before"] == "before_id" - assert request_kwargs["params"]["order"] == "desc" - assert "after" not in request_kwargs["params"] - assert len(result.data) == 2 - assert result.data[0].id == "session_1" - assert result.list_metadata.before is None - assert result.list_metadata.after is None - - def test_list_sessions_auto_pagination( - self, test_auto_pagination: TestAutoPaginationFunction - ): - data = [_mock_session(str(i)) for i in range(40)] - test_auto_pagination( - http_client=self.http_client, - list_function=self.user_management.list_sessions, - list_function_params={"user_id": "user_123"}, - expected_all_page_data=data, - url_path_keys=["user_id"], - ) diff --git a/tests/test_user_management_revoke_session.py b/tests/test_user_management_revoke_session.py deleted file mode 100644 index 6f6247ca..00000000 --- a/tests/test_user_management_revoke_session.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import Union - -import pytest - -from tests.utils.syncify import syncify -from workos.user_management import AsyncUserManagement, UserManagement - - -@pytest.mark.sync_and_async(UserManagement, AsyncUserManagement) -class TestUserManagementRevokeSession: - @pytest.fixture(autouse=True) - def setup(self, module_instance: Union[UserManagement, AsyncUserManagement]): - self.http_client = module_instance._http_client - self.user_management = module_instance - - def test_revoke_session(self, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 200) - - response = syncify( - self.user_management.revoke_session(session_id="session_abc") - ) - - assert request_kwargs["url"].endswith("user_management/sessions/revoke") - assert request_kwargs["method"] == "post" - assert request_kwargs["json"] == {"session_id": "session_abc"} - assert response is None diff --git a/tests/test_vault.py b/tests/test_vault.py deleted file mode 100644 index 0af5fea4..00000000 --- a/tests/test_vault.py +++ /dev/null @@ -1,489 +0,0 @@ -from typing import Any, cast - -import pytest - -from tests.utils.fixtures.mock_vault_object import ( - MockObjectDigest, - MockObjectMetadata, - MockObjectVersion, - MockVaultObject, -) -from tests.utils.list_resource import list_response_of -from workos.types.vault.key import KeyContext -from workos.vault import Vault - - -class TestVault: - @pytest.fixture(autouse=True) - def setup(self, sync_http_client_for_test): - self.http_client = sync_http_client_for_test - self.vault = Vault(http_client=self.http_client) - - @pytest.fixture - def mock_vault_object(self): - return MockVaultObject( - "vault_01234567890abcdef", "test-secret", "secret-value" - ).dict() - - @pytest.fixture - def mock_object_digest(self): - return MockObjectDigest("vault_01234567890abcdef", "test-secret").dict() - - @pytest.fixture - def mock_object_metadata(self): - return MockObjectMetadata("vault_01234567890abcdef").dict() - - @pytest.fixture - def mock_vault_object_no_value(self): - mock_obj = MockVaultObject("vault_01234567890abcdef", "test-secret") - mock_obj.value = None - return mock_obj.dict() - - @pytest.fixture - def mock_vault_objects_list(self): - vault_objects = [ - MockObjectDigest(f"vault_{i}", f"secret-{i}").dict() for i in range(5) - ] - return { - "data": vault_objects, - "list_metadata": {"before": None, "after": None}, - "object": "list", - } - - @pytest.fixture - def mock_vault_objects_multiple_pages(self): - vault_objects = [ - MockObjectDigest(f"vault_{i}", f"secret-{i}").dict() for i in range(25) - ] - return list_response_of(data=vault_objects) - - @pytest.fixture - def mock_object_versions(self): - versions = [ - MockObjectVersion(f"version_{i}", current_version=(i == 0)).dict() - for i in range(3) - ] - return {"data": versions} - - @pytest.fixture - def mock_data_key(self): - return { - "id": "key_01234567890abcdef", - "data_key": "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=", - } - - @pytest.fixture - def mock_data_key_pair(self): - return { - "context": {"key": "test-key"}, - "id": "key_01234567890abcdef", - "data_key": "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=", - "encrypted_keys": "ZW5jcnlwdGVkX2tleXNfZGF0YQ==", - } - - def test_read_object_success( - self, mock_vault_object, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_vault_object, 200 - ) - - vault_object = self.vault.read_object(object_id="vault_01234567890abcdef") - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/vault/v1/kv/vault_01234567890abcdef") - assert vault_object.id == "vault_01234567890abcdef" - assert vault_object.name == "test-secret" - assert vault_object.value == "secret-value" - assert vault_object.metadata.environment_id == "env_01234567890abcdef" - - def test_read_object_missing_object_id(self): - with pytest.raises( - ValueError, match="Incomplete arguments: 'object_id' is a required argument" - ): - self.vault.read_object(object_id="") - - def test_read_object_none_object_id(self): - with pytest.raises( - ValueError, match="Incomplete arguments: 'object_id' is a required argument" - ): - self.vault.read_object(object_id=cast(str, None)) - - def test_read_object_by_name_success( - self, mock_vault_object, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_vault_object, 200 - ) - - vault_object = self.vault.read_object_by_name(name="test-secret") - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/vault/v1/kv/name/test-secret") - assert vault_object.id == "vault_01234567890abcdef" - assert vault_object.name == "test-secret" - assert vault_object.value == "secret-value" - assert vault_object.metadata.environment_id == "env_01234567890abcdef" - - def test_read_object_by_name_missing_name(self): - with pytest.raises( - ValueError, match="Incomplete arguments: 'name' is a required argument" - ): - self.vault.read_object_by_name(name="") - - def test_read_object_by_name_none_name(self): - with pytest.raises( - ValueError, match="Incomplete arguments: 'name' is a required argument" - ): - self.vault.read_object_by_name(name=cast(str, None)) - - def test_list_objects_default_params( - self, mock_vault_objects_list, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_vault_objects_list, 200 - ) - - vault_objects = self.vault.list_objects() - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/vault/v1/kv") - assert request_kwargs["params"]["limit"] == 10 - assert "before" not in request_kwargs["params"] - assert "after" not in request_kwargs["params"] - assert len(vault_objects.data) == 5 - assert vault_objects.data[0].id == "vault_0" - assert vault_objects.data[0].name == "secret-0" - - def test_list_objects_with_params( - self, mock_vault_objects_list, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_vault_objects_list, 200 - ) - - self.vault.list_objects(limit=5, before="vault_before", after="vault_after") - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith("/vault/v1/kv") - assert request_kwargs["params"]["limit"] == 5 - assert request_kwargs["params"]["before"] == "vault_before" - assert request_kwargs["params"]["after"] == "vault_after" - - def test_list_objects_auto_pagination( - self, mock_vault_objects_multiple_pages, test_auto_pagination - ): - test_auto_pagination( - http_client=self.http_client, - list_function=self.vault.list_objects, - expected_all_page_data=mock_vault_objects_multiple_pages["data"], - ) - - def test_list_object_versions_success( - self, mock_object_versions, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_object_versions, 200 - ) - - versions = self.vault.list_object_versions(object_id="vault_01234567890abcdef") - - assert request_kwargs["method"] == "get" - assert request_kwargs["url"].endswith( - "/vault/v1/kv/vault_01234567890abcdef/versions" - ) - assert len(versions) == 3 - assert versions[0].id == "version_0" - assert versions[0].current_version is True - assert versions[1].current_version is False - - def test_list_object_versions_empty_data( - self, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, {"data": []}, 200 - ) - - versions = self.vault.list_object_versions(object_id="vault_01234567890abcdef") - - assert request_kwargs["method"] == "get" - assert len(versions) == 0 - - def test_create_object_success( - self, mock_object_metadata, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_object_metadata, 200 - ) - - object_metadata = self.vault.create_object( - name="test-secret", - value="secret-value", - key_context=KeyContext({"key": "test-key"}), - ) - - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith("/vault/v1/kv") - assert request_kwargs["json"]["name"] == "test-secret" - assert request_kwargs["json"]["value"] == "secret-value" - assert request_kwargs["json"]["key_context"] == KeyContext({"key": "test-key"}) - assert object_metadata.id == "vault_01234567890abcdef" - - def test_create_object_missing_name(self): - with pytest.raises( - ValueError, - match="Incomplete arguments: 'name' and 'value' are required arguments", - ): - self.vault.create_object( - name="", - value="secret-value", - key_context=KeyContext({"key": "test-key"}), - ) - - def test_create_object_missing_value(self): - with pytest.raises( - ValueError, - match="Incomplete arguments: 'name' and 'value' are required arguments", - ): - self.vault.create_object( - name="test-secret", - value="", - key_context=KeyContext({"key": "test-key"}), - ) - - def test_create_object_missing_both(self): - with pytest.raises( - ValueError, - match="Incomplete arguments: 'name' and 'value' are required arguments", - ): - self.vault.create_object( - name="", value="", key_context=KeyContext({"key": "test-key"}) - ) - - def test_update_object_with_value( - self, mock_vault_object, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_vault_object, 200 - ) - - vault_object = self.vault.update_object( - object_id="vault_01234567890abcdef", - value="updated-value", - ) - - assert request_kwargs["method"] == "put" - assert request_kwargs["url"].endswith("/vault/v1/kv/vault_01234567890abcdef") - assert request_kwargs["json"]["value"] == "updated-value" - assert "version_check" not in request_kwargs["json"] - assert vault_object.id == "vault_01234567890abcdef" - - def test_update_object_with_version_check( - self, mock_vault_object, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_vault_object, 200 - ) - - self.vault.update_object( - object_id="vault_01234567890abcdef", - value="updated-value", - version_check="version_123", - ) - - assert request_kwargs["method"] == "put" - assert request_kwargs["json"]["value"] == "updated-value" - assert request_kwargs["json"]["version_check"] == "version_123" - - def test_update_object_missing_value(self): - with pytest.raises( - TypeError, match="missing 1 required keyword-only argument: 'value'" - ): - kwargs: dict[str, Any] = {"object_id": "vault_01234567890abcdef"} - self.vault.update_object(**kwargs) - - def test_update_object_missing_object_id(self): - with pytest.raises( - ValueError, match="Incomplete arguments: 'object_id' is a required argument" - ): - self.vault.update_object(object_id="", value="test-value") - - def test_update_object_none_object_id(self): - with pytest.raises( - ValueError, - match="Incomplete arguments: 'object_id' is a required argument", - ): - self.vault.update_object(object_id=cast(str, None), value="updated-value") - - def test_delete_object_success(self, capture_and_mock_http_client_request): - request_kwargs = capture_and_mock_http_client_request(self.http_client, {}, 204) - - result = self.vault.delete_object(object_id="vault_01234567890abcdef") - - assert request_kwargs["method"] == "delete" - assert request_kwargs["url"].endswith("/vault/v1/kv/vault_01234567890abcdef") - assert result is None - - def test_delete_object_missing_object_id(self): - with pytest.raises( - ValueError, match="Incomplete arguments: 'object_id' is a required argument" - ): - self.vault.delete_object(object_id="") - - def test_delete_object_none_object_id(self): - with pytest.raises( - ValueError, match="Incomplete arguments: 'object_id' is a required argument" - ): - self.vault.delete_object(object_id=cast(str, None)) - - def test_create_data_key_success( - self, mock_data_key_pair, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_data_key_pair, 200 - ) - - data_key_pair = self.vault.create_data_key( - key_context=KeyContext({"key": "test-key"}) - ) - - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith("/vault/v1/keys/data-key") - assert request_kwargs["json"]["context"] == KeyContext({"key": "test-key"}) - assert data_key_pair.data_key.id == "key_01234567890abcdef" - assert data_key_pair.encrypted_keys == "ZW5jcnlwdGVkX2tleXNfZGF0YQ==" - - def test_decrypt_data_key_success( - self, mock_data_key, capture_and_mock_http_client_request - ): - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_data_key, 200 - ) - - data_key = self.vault.decrypt_data_key(keys="ZW5jcnlwdGVkX2tleXNfZGF0YQ==") - - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith("/vault/v1/keys/decrypt") - assert request_kwargs["json"]["keys"] == "ZW5jcnlwdGVkX2tleXNfZGF0YQ==" - assert data_key.id == "key_01234567890abcdef" - assert data_key.key == "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=" - - def test_encrypt_success( - self, mock_data_key_pair, capture_and_mock_http_client_request - ): - # Mock the create_data_key call - request_kwargs = capture_and_mock_http_client_request( - self.http_client, mock_data_key_pair, 200 - ) - - plaintext = "Hello, World!" - context = KeyContext({"key": "test-key"}) - - encrypted_data = self.vault.encrypt(data=plaintext, key_context=context) - - # Verify create_data_key was called - assert request_kwargs["method"] == "post" - assert request_kwargs["url"].endswith("/vault/v1/keys/data-key") - assert request_kwargs["json"]["context"] == KeyContext({"key": "test-key"}) - - # Verify we got encrypted data back - assert isinstance(encrypted_data, str) - assert len(encrypted_data) > 0 - - def test_encrypt_with_associated_data( - self, mock_data_key_pair, capture_and_mock_http_client_request - ): - # Mock the create_data_key call - capture_and_mock_http_client_request(self.http_client, mock_data_key_pair, 200) - - plaintext = "Hello, World!" - context = KeyContext({"key": "test-key"}) - associated_data = "additional-context" - - encrypted_data = self.vault.encrypt( - data=plaintext, key_context=context, associated_data=associated_data - ) - - # Verify we got encrypted data back - assert isinstance(encrypted_data, str) - assert len(encrypted_data) > 0 - - def test_decrypt_success(self, mock_data_key, capture_and_mock_http_client_request): - # First encrypt some data to get a valid encrypted payload - mock_data_key_pair = { - "context": {"key": "test-key"}, - "id": "key_01234567890abcdef", - "data_key": "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=", - "encrypted_keys": "ZW5jcnlwdGVkX2tleXNfZGF0YQ==", - } - - # Mock create_data_key for encryption - capture_and_mock_http_client_request(self.http_client, mock_data_key_pair, 200) - - plaintext = "Hello, World!" - context = KeyContext({"key": "test-key"}) - encrypted_data = self.vault.encrypt(data=plaintext, key_context=context) - - # Now mock decrypt_data_key for decryption - capture_and_mock_http_client_request(self.http_client, mock_data_key, 200) - - # Decrypt the data - decrypted_text = self.vault.decrypt(encrypted_data=encrypted_data) - - # Verify decryption worked - assert decrypted_text == plaintext - - def test_decrypt_with_associated_data( - self, mock_data_key, capture_and_mock_http_client_request - ): - # First encrypt some data with associated data - mock_data_key_pair = { - "context": {"key": "test-key"}, - "id": "key_01234567890abcdef", - "data_key": "MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=", - "encrypted_keys": "ZW5jcnlwdGVkX2tleXNfZGF0YQ==", - } - - # Mock create_data_key for encryption - capture_and_mock_http_client_request(self.http_client, mock_data_key_pair, 200) - - plaintext = "Hello, World!" - context = KeyContext({"key": "test-key"}) - associated_data = "additional-context" - encrypted_data = self.vault.encrypt( - data=plaintext, key_context=context, associated_data=associated_data - ) - - # Now mock decrypt_data_key for decryption - capture_and_mock_http_client_request(self.http_client, mock_data_key, 200) - - # Decrypt the data with the same associated data - decrypted_text = self.vault.decrypt( - encrypted_data=encrypted_data, associated_data=associated_data - ) - - # Verify decryption worked - assert decrypted_text == plaintext - - def test_encrypt_decrypt_roundtrip( - self, mock_data_key_pair, mock_data_key, capture_and_mock_http_client_request - ): - """Test that encrypt/decrypt works correctly together""" - - # Mock create_data_key for encryption - capture_and_mock_http_client_request(self.http_client, mock_data_key_pair, 200) - - plaintext = "This is a test message for encryption!" - context = KeyContext({"env": "test", "service": "vault"}) - - # Encrypt the data - encrypted_data = self.vault.encrypt(data=plaintext, key_context=context) - - # Mock decrypt_data_key for decryption - capture_and_mock_http_client_request(self.http_client, mock_data_key, 200) - - # Decrypt the data - decrypted_text = self.vault.decrypt(encrypted_data=encrypted_data) - - # Verify roundtrip worked - assert decrypted_text == plaintext diff --git a/tests/test_webhooks.py b/tests/test_webhooks.py deleted file mode 100644 index 5dc6f6d2..00000000 --- a/tests/test_webhooks.py +++ /dev/null @@ -1,156 +0,0 @@ -import json -import pytest -from workos.typing.webhooks import WebhookTypeAdapter -from workos.webhooks import Webhooks - - -class TestWebhooks(object): - @pytest.fixture(autouse=True) - def setup(self): - self.webhooks = Webhooks() - - @pytest.fixture - def mock_event_body(self): - return '{"id":"event_01J44T8116Q5M0RYCFA6KWNXN9","data":{"id":"conn_01EHWNC0FCBHZ3BJ7EGKYXK0E6","name":"Foo Corp\'s Connection","state":"active","object":"connection","status":"linked","domains":[{"id":"conn_domain_01EHWNFTAFCF3CQAE5A9Q0P1YB","domain":"foo-corp.com","object":"connection_domain"}],"created_at":"2021-06-25T19:07:33.155Z","updated_at":"2021-06-25T19:07:33.155Z","external_key":"3QMR4u0Tok6SgwY2AWG6u6mkQ","connection_type":"OktaSAML","organization_id":"org_01EHWNCE74X7JSDV0X3SZ3KJNY"},"event":"connection.activated","created_at":"2021-06-25T19:07:33.155Z"}' - - @pytest.fixture - def mock_header(self): - return "t=1722443701539, v1=bd54a3768f461461c8439c2f97ab0d646ef3976f84d5d5b132d18f2fa89cdad5" - - @pytest.fixture - def mock_secret(self): - return "2sAZJlbjP8Ce3rwkKEv2GfKef" - - @pytest.fixture - def mock_bad_secret(self): - return "this_is_not_it_123" - - @pytest.fixture - def mock_header_no_timestamp(self): - return "v1=bd54a3768f461461c8439c2f97ab0d646ef3976f84d5d5b132d18f2fa89cdad5" - - @pytest.fixture - def mock_sig_hash(self): - return "df25b6efdd39d82e7b30e75ea19655b306860ad5cde3eeaeb6f1dfea029ea259" - - @pytest.fixture - def mock_unknown_webhook_body(self): - return '{"id":"event_01J44T8116Q5M0RYCFA6KWNXN9","data":{"id":"meow_123","name":"Meow Corp","object":"kitten","status":"cuteness","created_at":"2021-06-25T19:07:33.155Z","updated_at":"2021-06-25T19:07:33.155Z"},"event":"kitten.created","created_at":"2021-06-25T19:07:33.155Z"}' - - @pytest.fixture - def mock_unknown_webhook_header(self): - return "t=1722443701539, v1=f82f88dd60d5bc8a803686a27f83ce148b8c37c54490c52b77d00d62da891f1b" - - def test_unable_to_extract_timestamp( - self, mock_event_body, mock_header_no_timestamp, mock_secret - ): - with pytest.raises(ValueError) as err: - self.webhooks.verify_event( - event_body=mock_event_body.encode("utf-8"), - event_signature=mock_header_no_timestamp, - secret=mock_secret, - tolerance=180, - ) - assert "Unable to extract timestamp and signature hash from header" in str( - err.value - ) - - def test_timestamp_outside_threshold( - self, mock_event_body, mock_header, mock_secret - ): - with pytest.raises(ValueError) as err: - self.webhooks.verify_event( - event_body=mock_event_body.encode("utf-8"), - event_signature=mock_header, - secret=mock_secret, - tolerance=0, - ) - assert "Timestamp outside the tolerance zone" in str(err.value) - - def test_sig_hash_does_not_match_expected_sig_length(self, mock_sig_hash): - result = self.webhooks._constant_time_compare( - mock_sig_hash, - "df25b6efdd39d82e7b30e75ea19655b306860ad5cde3eeaeb6f1dfea029ea25", - ) - assert result is False - - def test_sig_hash_does_not_match_expected_sig_value(self, mock_sig_hash): - result = self.webhooks._constant_time_compare( - mock_sig_hash, - "df25b6efdd39d82e7b30e75ea19655b306860ad5cde3eeaeb6f1dfea029ea252", - ) - assert result is False - - def test_passed_expected_event_validation( - self, mock_event_body, mock_header, mock_secret - ): - try: - webhook = self.webhooks.verify_event( - event_body=mock_event_body.encode("utf-8"), - event_signature=mock_header, - secret=mock_secret, - tolerance=99999999999999, - ) - assert type(webhook).__name__ == "ConnectionActivatedWebhook" - except BaseException: - pytest.fail( - "There was an error in validating the webhook with the expected values" - ) - - def test_sign_hash_does_not_match_expected_sig_hash_verify_header( - self, mock_event_body, mock_header, mock_bad_secret - ): - with pytest.raises(ValueError) as err: - self.webhooks.verify_header( - event_body=mock_event_body.encode("utf-8"), - event_signature=mock_header, - secret=mock_bad_secret, - tolerance=99999999999999, - ) - assert ( - "Signature hash does not match the expected signature hash for payload" - in str(err.value) - ) - - def test_unrecognized_webhook_type_returns_untyped_webhook( - self, mock_unknown_webhook_body, mock_unknown_webhook_header, mock_secret - ): - result = self.webhooks.verify_event( - event_body=mock_unknown_webhook_body.encode("utf-8"), - event_signature=mock_unknown_webhook_header, - secret=mock_secret, - tolerance=99999999999999, - ) - assert type(result).__name__ == "UntypedWebhook" - assert result.dict() == json.loads(mock_unknown_webhook_body) - - # TODO: This test should be updated in the next major version to expect - # a DirectoryActivatedWebhook return type. - def test_validate_dsync_activated_event(self): - event_body = { - "id": "event_01J8SX5FTXYD2YFWVTGJY49EM6", - "data": { - "id": "directory_01EHWNC0FCBHZ3BJ7EGKYXK0E6", - "name": "Foo Corp's Directory", - "type": "generic scim v2.0", - "state": "active", - "object": "directory", - "domains": [ - { - "id": "org_domain_01EZTR5N6Y9RQKHK2E9F31KZX6", - "domain": "foo-corp.com", - "object": "organization_domain", - } - ], - "created_at": "2021-06-25T19:07:33.155Z", - "updated_at": "2021-06-25T19:07:33.155Z", - "external_key": "UWuccu6o1E0GqkYs", - "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", - }, - "event": "dsync.activated", - "created_at": "2021-06-25T19:07:33.155Z", - } - - result = WebhookTypeAdapter.validate_json(json.dumps(event_body)) - assert type(result).__name__ == "UntypedWebhook" - assert result.dict() == event_body diff --git a/tests/test_widgets.py b/tests/test_widgets.py deleted file mode 100644 index f008cb95..00000000 --- a/tests/test_widgets.py +++ /dev/null @@ -1,25 +0,0 @@ -import pytest - -from workos.widgets import Widgets - - -class TestWidgets(object): - @pytest.fixture(autouse=True) - def setup(self, sync_http_client_for_test): - self.http_client = sync_http_client_for_test - self.widgets = Widgets(http_client=self.http_client) - - @pytest.fixture - def mock_widget_token(self): - return {"token": "abc123456"} - - def test_get_token(self, mock_widget_token, mock_http_client_with_response): - mock_http_client_with_response(self.http_client, mock_widget_token, 201) - - response = self.widgets.get_token( - organization_id="org_01EHQMYV6MBK39QC5PZXHY59C3", - user_id="user_01EHQMYV6MBK39QC5PZXHY59C3", - scopes=["widgets:users-table:manage"], - ) - - assert response.token == "abc123456" diff --git a/tests/types/test_auto_pagination_function.py b/tests/types/test_auto_pagination_function.py deleted file mode 100644 index 857c02a6..00000000 --- a/tests/types/test_auto_pagination_function.py +++ /dev/null @@ -1,4 +0,0 @@ -from typing import Callable - - -TestAutoPaginationFunction = Callable[..., None] diff --git a/tests/types/test_directory_state.py b/tests/types/test_directory_state.py deleted file mode 100644 index 00994eb2..00000000 --- a/tests/types/test_directory_state.py +++ /dev/null @@ -1,33 +0,0 @@ -import pytest -from pydantic import TypeAdapter, ValidationError -from workos.types.directory_sync.directory_state import DirectoryState - - -class TestDirectoryState: - @pytest.fixture - def directory_state_type_adapter(self): - return TypeAdapter(DirectoryState) - - def test_convert_linked_to_active(self, directory_state_type_adapter): - assert directory_state_type_adapter.validate_python("active") == "active" - assert directory_state_type_adapter.validate_python("linked") == "active" - try: - directory_state_type_adapter.validate_python("foo") - except ValidationError as e: - assert e.errors()[0]["type"] == "literal_error" - - def test_convert_unlinked_to_inactive(self, directory_state_type_adapter): - assert directory_state_type_adapter.validate_python("unlinked") == "inactive" - assert directory_state_type_adapter.validate_python("inactive") == "inactive" - - @pytest.mark.parametrize( - "type_value", ["foo", None, 5, {"definitely": "not a state"}] - ) - def test_invalid_values_returns_validation_error( - self, directory_state_type_adapter, type_value - ): - try: - directory_state_type_adapter.validate_python(type_value) - pytest.fail(f"Expected ValidationError for: {type_value}") - except ValidationError as e: - assert e.errors()[0]["type"] == "literal_error" diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/utils/client_configuration.py b/tests/utils/client_configuration.py deleted file mode 100644 index eeb28478..00000000 --- a/tests/utils/client_configuration.py +++ /dev/null @@ -1,33 +0,0 @@ -from workos._client_configuration import ( - ClientConfiguration as ClientConfigurationProtocol, -) - - -class ClientConfiguration(ClientConfigurationProtocol): - def __init__( - self, - base_url: str, - client_id: str, - request_timeout: int, - jwt_leeway: float = 0, - ): - self._base_url = base_url - self._client_id = client_id - self._request_timeout = request_timeout - self._jwt_leeway = jwt_leeway - - @property - def base_url(self) -> str: - return self._base_url - - @property - def client_id(self) -> str: - return self._client_id - - @property - def request_timeout(self) -> int: - return self._request_timeout - - @property - def jwt_leeway(self) -> float: - return self._jwt_leeway diff --git a/tests/utils/fixtures/__init__.py b/tests/utils/fixtures/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/utils/fixtures/mock_api_key.py b/tests/utils/fixtures/mock_api_key.py deleted file mode 100644 index 0bd2d7cb..00000000 --- a/tests/utils/fixtures/mock_api_key.py +++ /dev/null @@ -1,37 +0,0 @@ -import datetime - -from workos.types.api_keys import ApiKey, ApiKeyWithValue -from workos.types.api_keys.api_keys import ApiKeyOwner - - -class MockApiKey(ApiKey): - def __init__(self, id="api_key_01234567890"): - now = datetime.datetime.now().isoformat() - super().__init__( - object="api_key", - id=id, - owner=ApiKeyOwner(type="organization", id="org_1337"), - name="Development API Key", - obfuscated_value="api_..0", - permissions=[], - last_used_at=now, - created_at=now, - updated_at=now, - ) - - -class MockApiKeyWithValue(ApiKeyWithValue): - def __init__(self, id="api_key_01234567890"): - now = datetime.datetime.now().isoformat() - super().__init__( - object="api_key", - id=id, - owner=ApiKeyOwner(type="organization", id="org_1337"), - name="Development API Key", - obfuscated_value="sk_...xyz", - value="sk_live_abc123xyz", - permissions=["posts:read", "posts:write"], - last_used_at=None, - created_at=now, - updated_at=now, - ) diff --git a/tests/utils/fixtures/mock_auth_factor_totp.py b/tests/utils/fixtures/mock_auth_factor_totp.py deleted file mode 100644 index 24b5f033..00000000 --- a/tests/utils/fixtures/mock_auth_factor_totp.py +++ /dev/null @@ -1,23 +0,0 @@ -import datetime - -from workos.types.mfa import AuthenticationFactorTotp, ExtendedTotpFactor - - -class MockAuthenticationFactorTotp(AuthenticationFactorTotp): - def __init__(self, id): - now = datetime.datetime.now().isoformat() - super().__init__( - object="authentication_factor", - id=id, - created_at=now, - updated_at=now, - type="totp", - user_id="user_123", - totp=ExtendedTotpFactor( - issuer="FooCorp", - user="test@example.com", - qr_code="data:image/png;base64,{base64EncodedPng}", - secret="NAGCCFS3EYRB422HNAKAKY3XDUORMSRF", - uri="otpauth://totp/FooCorp:alan.turing@foo-corp.com?secret=NAGCCFS3EYRB422HNAKAKY3XDUORMSRF&issuer=FooCorp", - ), - ) diff --git a/tests/utils/fixtures/mock_client_secret.py b/tests/utils/fixtures/mock_client_secret.py deleted file mode 100644 index 70bec678..00000000 --- a/tests/utils/fixtures/mock_client_secret.py +++ /dev/null @@ -1,19 +0,0 @@ -import datetime - -from workos.types.connect import ClientSecret - - -class MockClientSecret(ClientSecret): - def __init__(self, id: str, include_secret: bool = False): - now = datetime.datetime.now().isoformat() - kwargs = { - "object": "connect_application_secret", - "id": id, - "secret_hint": "...abcd", - "last_used_at": None, - "created_at": now, - "updated_at": now, - } - if include_secret: - kwargs["secret"] = "sk_test_secret_value_123" - super().__init__(**kwargs) diff --git a/tests/utils/fixtures/mock_connect_application.py b/tests/utils/fixtures/mock_connect_application.py deleted file mode 100644 index f4c12305..00000000 --- a/tests/utils/fixtures/mock_connect_application.py +++ /dev/null @@ -1,29 +0,0 @@ -import datetime - -from workos.types.connect import ConnectApplication -from workos.types.connect.connect_application import ApplicationType - - -class MockConnectApplication(ConnectApplication): - def __init__(self, id: str, application_type: ApplicationType = "m2m"): - now = datetime.datetime.now().isoformat() - kwargs = { - "object": "connect_application", - "id": id, - "client_id": f"client_{id}", - "name": "Test Application", - "application_type": application_type, - "scopes": ["read", "write"], - "created_at": now, - "updated_at": now, - } - if application_type == "m2m": - kwargs["organization_id"] = "org_01ABC" - elif application_type == "oauth": - kwargs["redirect_uris"] = [ - {"uri": "https://example.com/callback", "default": True} - ] - kwargs["uses_pkce"] = True - kwargs["is_first_party"] = True - kwargs["was_dynamically_registered"] = False - super().__init__(**kwargs) diff --git a/tests/utils/fixtures/mock_connection.py b/tests/utils/fixtures/mock_connection.py deleted file mode 100644 index c68173d2..00000000 --- a/tests/utils/fixtures/mock_connection.py +++ /dev/null @@ -1,29 +0,0 @@ -import datetime -from workos.types.sso import ( - ConnectionDomain, - ConnectionWithDomains, - SamlConnectionOptions, -) - - -class MockConnection(ConnectionWithDomains): - def __init__(self, id): - now = datetime.datetime.now().isoformat() - super().__init__( - object="connection", - id=id, - organization_id="org_id_" + id, - connection_type="OktaSAML", - name="Foo Corporation", - state="active", - created_at=now, - updated_at=now, - options=SamlConnectionOptions(signing_cert="signing_cert"), - domains=[ - ConnectionDomain( - id="connection_domain_abc123", - object="connection_domain", - domain="domain1.com", - ) - ], - ) diff --git a/tests/utils/fixtures/mock_directory.py b/tests/utils/fixtures/mock_directory.py deleted file mode 100644 index e2b17d54..00000000 --- a/tests/utils/fixtures/mock_directory.py +++ /dev/null @@ -1,35 +0,0 @@ -import datetime - -from workos.types.directory_sync import ( - Directory, - DirectoryMetadata, - DirectoryUsersMetadata, -) - - -class MockDirectoryUsersMetadata(DirectoryUsersMetadata): - def __init__(self, active=0, inactive=0): - super().__init__(active=active, inactive=inactive) - - -class MockDirectoryMetadata(DirectoryMetadata): - def __init__(self, users, groups=0): - super().__init__(users=users, groups=groups) - - -class MockDirectory(Directory): - def __init__(self, id, metadata=None): - now = datetime.datetime.now().isoformat() - super().__init__( - object="directory", - id=id, - organization_id="organization_id", - external_key="ext_123", - domain="somefakedomain.com", - name="Some fake name", - state="active", - type="gsuite directory", - metadata=metadata, - created_at=now, - updated_at=now, - ) diff --git a/tests/utils/fixtures/mock_directory_group.py b/tests/utils/fixtures/mock_directory_group.py deleted file mode 100644 index 62b3da40..00000000 --- a/tests/utils/fixtures/mock_directory_group.py +++ /dev/null @@ -1,19 +0,0 @@ -import datetime - -from workos.types.directory_sync import DirectoryGroup - - -class MockDirectoryGroup(DirectoryGroup): - def __init__(self, id): - now = datetime.datetime.now().isoformat() - super().__init__( - object="directory_group", - id=id, - idp_id="idp_id_" + id, - directory_id="directory_id", - organization_id="organization_id", - name="group_" + id, - created_at=now, - updated_at=now, - raw_attributes={}, - ) diff --git a/tests/utils/fixtures/mock_directory_user.py b/tests/utils/fixtures/mock_directory_user.py deleted file mode 100644 index 2899e456..00000000 --- a/tests/utils/fixtures/mock_directory_user.py +++ /dev/null @@ -1,53 +0,0 @@ -import datetime - -from workos.types.directory_sync import DirectoryUserWithGroups -from workos.types.directory_sync.directory_user import DirectoryUserEmail, InlineRole - - -class MockDirectoryUser(DirectoryUserWithGroups): - def __init__(self, id): - now = datetime.datetime.now().isoformat() - super().__init__( - object="directory_user", - id=id, - idp_id="idp_id_" + id, - directory_id="directory_id", - organization_id="org_id_" + id, - first_name="gsuite_directory", - last_name="fried chicken", - job_title="developer", - email="marcelina@foo-corp.com", - emails=[ - DirectoryUserEmail( - primary=True, type="work", value="marcelina@foo-corp.com" - ) - ], - username=None, - groups=[], - state="active", - created_at=now, - updated_at=now, - custom_attributes={}, - raw_attributes={ - "schemas": ["urn:scim:schemas:core:1.0"], - "name": {"familyName": "Seri", "givenName": "Marcelina"}, - "externalId": "external-id", - "locale": "en_US", - "userName": "marcelina@foo-corp.com", - "id": "directory_usr_id", - "displayName": "Marcelina Seri", - "title": "developer", - "active": True, - "groups": [], - "meta": None, - "emails": [ - { - "value": "marcelina@foo-corp.com", - "type": "work", - "primary": "true", - } - ], - }, - role=InlineRole(slug="member"), - roles=[InlineRole(slug="member")], - ) diff --git a/tests/utils/fixtures/mock_email_verification.py b/tests/utils/fixtures/mock_email_verification.py deleted file mode 100644 index 5c341b64..00000000 --- a/tests/utils/fixtures/mock_email_verification.py +++ /dev/null @@ -1,18 +0,0 @@ -import datetime - -from workos.types.user_management import EmailVerification - - -class MockEmailVerification(EmailVerification): - def __init__(self, id): - now = datetime.datetime.now().isoformat() - super().__init__( - object="email_verification", - id=id, - user_id="user_01HWZBQAY251RZ9BKB4RZW4D4A", - email="marcelina@foo-corp.com", - expires_at=now, - code="123456", - created_at=now, - updated_at=now, - ) diff --git a/tests/utils/fixtures/mock_environment_role.py b/tests/utils/fixtures/mock_environment_role.py deleted file mode 100644 index b3914929..00000000 --- a/tests/utils/fixtures/mock_environment_role.py +++ /dev/null @@ -1,20 +0,0 @@ -import datetime - -from workos.types.authorization.environment_role import EnvironmentRole - - -class MockEnvironmentRole(EnvironmentRole): - def __init__(self, id: str): - now = datetime.datetime.now().isoformat() - super().__init__( - object="role", - id=id, - name="Member", - slug="member", - description="Default environment member role", - resource_type_slug="organization", - permissions=["documents:read"], - type="EnvironmentRole", - created_at=now, - updated_at=now, - ) diff --git a/tests/utils/fixtures/mock_event.py b/tests/utils/fixtures/mock_event.py deleted file mode 100644 index aaac685b..00000000 --- a/tests/utils/fixtures/mock_event.py +++ /dev/null @@ -1,29 +0,0 @@ -import datetime - -from workos.types.events import DirectoryActivatedEvent -from workos.types.events.directory_payload_with_legacy_fields import ( - DirectoryPayloadWithLegacyFieldsForEventsApi, -) - - -class MockEvent(DirectoryActivatedEvent): - def __init__(self, id): - now = datetime.datetime.now().isoformat() - super().__init__( - object="event", - id=id, - event="dsync.activated", - data=DirectoryPayloadWithLegacyFieldsForEventsApi( - object="directory", - id="dir_1234", - organization_id="organization_id", - external_key="ext_123", - domains=[], - name="Some fake name", - state="active", - type="gsuite directory", - created_at=now, - updated_at=now, - ), - created_at=now, - ) diff --git a/tests/utils/fixtures/mock_feature_flag.py b/tests/utils/fixtures/mock_feature_flag.py deleted file mode 100644 index 1100e816..00000000 --- a/tests/utils/fixtures/mock_feature_flag.py +++ /dev/null @@ -1,17 +0,0 @@ -import datetime - -from workos.types.feature_flags.feature_flag import FeatureFlag - - -class MockFeatureFlag(FeatureFlag): - def __init__(self, id): - now = datetime.datetime.now().isoformat() - super().__init__( - object="feature_flag", - id=id, - slug="test-feature", - name="Test Feature", - description="A test feature flag", - created_at=now, - updated_at=now, - ) diff --git a/tests/utils/fixtures/mock_invitation.py b/tests/utils/fixtures/mock_invitation.py deleted file mode 100644 index 52652f9c..00000000 --- a/tests/utils/fixtures/mock_invitation.py +++ /dev/null @@ -1,23 +0,0 @@ -import datetime - -from workos.types.user_management import Invitation - - -class MockInvitation(Invitation): - def __init__(self, id): - now = datetime.datetime.now().isoformat() - super().__init__( - object="invitation", - id=id, - email="marcelina@foo-corp.com", - state="pending", - accepted_at=None, - revoked_at=None, - expires_at=now, - token="Z1uX3RbwcIl5fIGJJJCXXisdI", - accept_invitation_url="https://your-app.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI", - organization_id="org_12345", - inviter_user_id="user_123", - created_at=now, - updated_at=now, - ) diff --git a/tests/utils/fixtures/mock_magic_auth.py b/tests/utils/fixtures/mock_magic_auth.py deleted file mode 100644 index 72bf51fe..00000000 --- a/tests/utils/fixtures/mock_magic_auth.py +++ /dev/null @@ -1,18 +0,0 @@ -import datetime - -from workos.types.user_management import MagicAuth - - -class MockMagicAuth(MagicAuth): - def __init__(self, id): - now = datetime.datetime.now().isoformat() - super().__init__( - object="magic_auth", - id=id, - user_id="user_01HWZBQAY251RZ9BKB4RZW4D4A", - email="marcelina@foo-corp.com", - expires_at=now, - code="123456", - created_at=now, - updated_at=now, - ) diff --git a/tests/utils/fixtures/mock_organization.py b/tests/utils/fixtures/mock_organization.py deleted file mode 100644 index 08e7044b..00000000 --- a/tests/utils/fixtures/mock_organization.py +++ /dev/null @@ -1,28 +0,0 @@ -import datetime - -from workos.types.organizations import Organization -from workos.types.organization_domains import OrganizationDomain - - -class MockOrganization(Organization): - def __init__(self, id): - now = datetime.datetime.now().isoformat() - super().__init__( - object="organization", - id=id, - name="Foo Corporation", - allow_profiles_outside_organization=False, - created_at=now, - updated_at=now, - domains=[ - OrganizationDomain( - object="organization_domain", - id="org_domain_01EHT88Z8WZEFWYPM6EC9BX2R8", - organization_id="org_12345", - domain="example.io", - created_at=now, - updated_at=now, - ) - ], - metadata={"key": "value"}, - ) diff --git a/tests/utils/fixtures/mock_organization_membership.py b/tests/utils/fixtures/mock_organization_membership.py deleted file mode 100644 index 408db909..00000000 --- a/tests/utils/fixtures/mock_organization_membership.py +++ /dev/null @@ -1,67 +0,0 @@ -import datetime -from typing import Optional, Sequence - -from workos.types.authorization.organization_membership import ( - AuthorizationOrganizationMembership, -) -from workos.types.list_resource import ListMetadata, ListPage -from workos.types.user_management import OrganizationMembership - - -class MockAuthorizationOrganizationMembershipList( - ListPage[AuthorizationOrganizationMembership] -): - def __init__( - self, - data: Optional[Sequence[AuthorizationOrganizationMembership]] = None, - before: Optional[str] = None, - after: Optional[str] = "om_01DEF", - ): - if data is None: - data = [ - AuthorizationOrganizationMembership( - object="organization_membership", - id="om_01ABC", - user_id="user_123", - organization_id="org_456", - organization_name="Foo Corp", - status="active", - directory_managed=False, - created_at="2024-01-01T00:00:00Z", - updated_at="2024-01-01T00:00:00Z", - ), - AuthorizationOrganizationMembership( - object="organization_membership", - id="om_01DEF", - user_id="user_789", - organization_id="org_456", - organization_name="Foo Corp", - status="active", - directory_managed=False, - created_at="2024-01-02T00:00:00Z", - updated_at="2024-01-02T00:00:00Z", - ), - ] - super().__init__( - object="list", - data=data, - list_metadata=ListMetadata(before=before, after=after), - ) - - -class MockOrganizationMembership(OrganizationMembership): - def __init__(self, id): - now = datetime.datetime.now().isoformat() - super().__init__( - object="organization_membership", - id=id, - user_id="user_12345", - organization_id="org_67890", - organization_name="Foo Corp", - status="active", - directory_managed=False, - role={"slug": "member"}, - custom_attributes={}, - created_at=now, - updated_at=now, - ) diff --git a/tests/utils/fixtures/mock_organization_role.py b/tests/utils/fixtures/mock_organization_role.py deleted file mode 100644 index 61f7a9b6..00000000 --- a/tests/utils/fixtures/mock_organization_role.py +++ /dev/null @@ -1,27 +0,0 @@ -import datetime -from typing import Any - -from workos.types.authorization.organization_role import OrganizationRole - - -class MockOrganizationRole(OrganizationRole): - def __init__( - self, - id: str, - organization_id: str = "org_01EHT88Z8J8795GZNQ4ZP1J81T", - ): - now = datetime.datetime.now().isoformat() - extra: dict[str, Any] = {"organization_id": organization_id} - super().__init__( - object="role", - id=id, - name="Admin", - slug="admin", - description="Organization admin role", - resource_type_slug="organization", - permissions=["documents:read", "documents:write"], - type="OrganizationRole", - created_at=now, - updated_at=now, - **extra, - ) diff --git a/tests/utils/fixtures/mock_password_reset.py b/tests/utils/fixtures/mock_password_reset.py deleted file mode 100644 index 4e639a46..00000000 --- a/tests/utils/fixtures/mock_password_reset.py +++ /dev/null @@ -1,18 +0,0 @@ -import datetime - -from workos.types.user_management import PasswordReset - - -class MockPasswordReset(PasswordReset): - def __init__(self, id): - now = datetime.datetime.now().isoformat() - super().__init__( - object="password_reset", - id=id, - user_id="user_01HWZBQAY251RZ9BKB4RZW4D4A", - email="marcelina@foo-corp.com", - password_reset_token="Z1uX3RbwcIl5fIGJJJCXXisdI", - password_reset_url="https://your-app.com/reset-password?token=Z1uX3RbwcIl5fIGJJJCXXisdI", - expires_at=now, - created_at=now, - ) diff --git a/tests/utils/fixtures/mock_permission.py b/tests/utils/fixtures/mock_permission.py deleted file mode 100644 index 277a4fd7..00000000 --- a/tests/utils/fixtures/mock_permission.py +++ /dev/null @@ -1,19 +0,0 @@ -import datetime - -from workos.types.authorization.permission import Permission - - -class MockPermission(Permission): - def __init__(self, id: str, slug: str = "documents:read"): - now = datetime.datetime.now().isoformat() - super().__init__( - object="permission", - id=id, - slug=slug, - name="Read Documents", - description="Allows reading documents", - resource_type_slug="organization", - system=False, - created_at=now, - updated_at=now, - ) diff --git a/tests/utils/fixtures/mock_profile.py b/tests/utils/fixtures/mock_profile.py deleted file mode 100644 index 290311c2..00000000 --- a/tests/utils/fixtures/mock_profile.py +++ /dev/null @@ -1,29 +0,0 @@ -from workos.types.sso import Profile - - -class MockProfile(Profile): - def __init__(self, id: str): - super().__init__( - object="profile", - id=id or "prof_01DWAS7ZQWM70PV93BFV1V78QV", - email="demo@workos-okta.com", - first_name="WorkOS", - last_name="Demo", - role={"slug": "admin"}, - roles=[{"slug": "admin"}], - groups=["Admins", "Developers"], - organization_id="org_01FG53X8636WSNW2WEKB2C31ZB", - connection_id="conn_01EMH8WAK20T42N2NBMNBCYHAG", - connection_type="OktaSAML", - idp_id="00u1klkowm8EGah2H357", - custom_attributes={ - "license": "professional", - }, - raw_attributes={ - "email": "demo@workos-okta.com", - "first_name": "WorkOS", - "last_name": "Demo", - "groups": ["Admins", "Developers"], - "license": "professional", - }, - ) diff --git a/tests/utils/fixtures/mock_resource.py b/tests/utils/fixtures/mock_resource.py deleted file mode 100644 index 957a01c5..00000000 --- a/tests/utils/fixtures/mock_resource.py +++ /dev/null @@ -1,30 +0,0 @@ -from typing import Optional - -from workos.types.authorization.authorization_resource import AuthorizationResource - - -class MockAuthorizationResource(AuthorizationResource): - def __init__( - self, - id: str = "res_01ABC", - external_id: str = "ext_123", - name: str = "Test Resource", - description: Optional[str] = "A test resource for unit tests", - resource_type_slug: str = "document", - organization_id: str = "org_01EHT88Z8J8795GZNQ4ZP1J81T", - parent_resource_id: Optional[str] = "res_01XYZ", - created_at: str = "2024-01-15T12:00:00.000Z", - updated_at: str = "2024-01-15T12:00:00.000Z", - ): - super().__init__( - object="authorization_resource", - id=id, - external_id=external_id, - name=name, - description=description, - resource_type_slug=resource_type_slug, - organization_id=organization_id, - parent_resource_id=parent_resource_id, - created_at=created_at, - updated_at=updated_at, - ) diff --git a/tests/utils/fixtures/mock_resource_list.py b/tests/utils/fixtures/mock_resource_list.py deleted file mode 100644 index a21c5c72..00000000 --- a/tests/utils/fixtures/mock_resource_list.py +++ /dev/null @@ -1,45 +0,0 @@ -from typing import Optional, Sequence - -from workos.types.authorization.authorization_resource import AuthorizationResource -from workos.types.list_resource import ListMetadata, ListPage - - -class MockAuthorizationResourceList(ListPage[AuthorizationResource]): - def __init__( - self, - data: Optional[Sequence[AuthorizationResource]] = None, - before: Optional[str] = None, - after: Optional[str] = "authz_resource_01HXYZ123ABC456DEF789DEF", - ): - if data is None: - data = [ - AuthorizationResource( - object="authorization_resource", - id="authz_resource_01HXYZ123ABC456DEF789ABC", - external_id="doc-12345678", - name="Q5 Budget Report", - description="Financial report for Q5 2025", - resource_type_slug="document", - organization_id="org_01HXYZ123ABC456DEF789ABC", - parent_resource_id="authz_resource_01HXYZ123ABC456DEF789XYZ", - created_at="2024-01-15T09:30:00.000Z", - updated_at="2024-01-15T09:30:00.000Z", - ), - AuthorizationResource( - object="authorization_resource", - id="authz_resource_01HXYZ123ABC456DEF789DEF", - external_id="folder-123", - name="Finance Folder", - description=None, - resource_type_slug="folder", - organization_id="org_01HXYZ123ABC456DEF789ABC", - parent_resource_id=None, - created_at="2024-01-14T08:00:00.000Z", - updated_at="2024-01-14T08:00:00.000Z", - ), - ] - super().__init__( - object="list", - data=data, - list_metadata=ListMetadata(before=before, after=after), - ) diff --git a/tests/utils/fixtures/mock_role.py b/tests/utils/fixtures/mock_role.py deleted file mode 100644 index 756e1af5..00000000 --- a/tests/utils/fixtures/mock_role.py +++ /dev/null @@ -1,18 +0,0 @@ -import datetime - -from workos.types.roles.role import Role - - -class MockRole(Role): - def __init__(self, id): - now = datetime.datetime.now().isoformat() - super().__init__( - object="role", - id=id, - name="Member", - slug="member", - description="The default member role", - type="EnvironmentRole", - created_at=now, - updated_at=now, - ) diff --git a/tests/utils/fixtures/mock_role_assignment.py b/tests/utils/fixtures/mock_role_assignment.py deleted file mode 100644 index e9c789f0..00000000 --- a/tests/utils/fixtures/mock_role_assignment.py +++ /dev/null @@ -1,68 +0,0 @@ -from typing import Optional, Sequence - -from workos.types.authorization.role_assignment import ( - RoleAssignment, - RoleAssignmentResource, - RoleAssignmentRole, -) -from workos.types.list_resource import ListMetadata, ListPage - - -class MockRoleAssignment(RoleAssignment): - def __init__( - self, - id: str = "ra_01ABC", - role_slug: str = "admin", - resource_id: str = "res_01ABC", - resource_external_id: str = "ext_123", - resource_type_slug: str = "document", - created_at: str = "2024-01-01T00:00:00Z", - updated_at: str = "2024-01-01T00:00:00Z", - ): - super().__init__( - object="role_assignment", - id=id, - role=RoleAssignmentRole(slug=role_slug), - resource=RoleAssignmentResource( - id=resource_id, - external_id=resource_external_id, - resource_type_slug=resource_type_slug, - ), - created_at=created_at, - updated_at=updated_at, - ) - - -class MockRoleAssignmentsList(ListPage[RoleAssignment]): - def __init__( - self, - data: Optional[Sequence[RoleAssignment]] = None, - before: Optional[str] = None, - after: Optional[str] = "ra_01DEF", - ): - if data is None: - data = [ - MockRoleAssignment( - id="ra_01ABC", - role_slug="admin", - resource_id="res_01ABC", - resource_external_id="ext_123", - resource_type_slug="document", - created_at="2024-01-15T09:30:00.000Z", - updated_at="2024-01-15T09:30:00.000Z", - ), - MockRoleAssignment( - id="ra_01DEF", - role_slug="editor", - resource_id="res_01XYZ", - resource_external_id="ext_456", - resource_type_slug="folder", - created_at="2024-01-14T08:00:00.000Z", - updated_at="2024-01-14T08:00:00.000Z", - ), - ] - super().__init__( - object="list", - data=data, - list_metadata=ListMetadata(before=before, after=after), - ) diff --git a/tests/utils/fixtures/mock_user.py b/tests/utils/fixtures/mock_user.py deleted file mode 100644 index b8537832..00000000 --- a/tests/utils/fixtures/mock_user.py +++ /dev/null @@ -1,22 +0,0 @@ -import datetime - -from workos.types.user_management import User - - -class MockUser(User): - def __init__(self, id): - now = datetime.datetime.now().isoformat() - super().__init__( - object="user", - id=id, - email="marcelina@foo-corp.com", - first_name="Marcelina", - last_name="Hoeger", - email_verified=False, - profile_picture_url="https://example.com/profile-picture.jpg", - last_sign_in_at="2021-06-25T19:07:33.155Z", - locale="en-US", - created_at=now, - updated_at=now, - metadata={"key": "value"}, - ) diff --git a/tests/utils/fixtures/mock_vault_object.py b/tests/utils/fixtures/mock_vault_object.py deleted file mode 100644 index 1a7b0f23..00000000 --- a/tests/utils/fixtures/mock_vault_object.py +++ /dev/null @@ -1,63 +0,0 @@ -import datetime - -from workos.types.vault import ( - VaultObject, - ObjectDigest, - ObjectMetadata, - ObjectUpdateBy, - ObjectVersion, - KeyContext, -) - - -class MockVaultObject(VaultObject): - def __init__( - self, id="vault_01234567890abcdef", name="test-secret", value="secret-value" - ): - now = datetime.datetime.now().isoformat() - super().__init__( - id=id, - name=name, - value=value, - metadata=ObjectMetadata( - context=KeyContext({"key": "test-key"}), - environment_id="env_01234567890abcdef", - id=id, - key_id="key_01234567890abcdef", - updated_at=now, - updated_by=ObjectUpdateBy( - id="user_01234567890abcdef", name="Test User" - ), - version_id="version_01234567890abcdef", - ), - ) - - -class MockObjectDigest(ObjectDigest): - def __init__(self, id="vault_01234567890abcdef", name="test-secret"): - now = datetime.datetime.now().isoformat() - super().__init__(id=id, name=name, updated_at=now) - - -class MockObjectMetadata(ObjectMetadata): - def __init__(self, id="vault_01234567890abcdef"): - now = datetime.datetime.now().isoformat() - super().__init__( - context=KeyContext({"key": "test-key"}), - environment_id="env_01234567890abcdef", - id=id, - key_id="key_01234567890abcdef", - updated_at=now, - updated_by=ObjectUpdateBy(id="user_01234567890abcdef", name="Test User"), - version_id="version_01234567890abcdef", - ) - - -class MockObjectVersion(ObjectVersion): - def __init__(self, id="version_01234567890abcdef", current_version=True): - now = datetime.datetime.now().isoformat() - super().__init__( - id=id, - created_at=now, - current_version=current_version, - ) diff --git a/tests/utils/list_resource.py b/tests/utils/list_resource.py deleted file mode 100644 index fef4a0ed..00000000 --- a/tests/utils/list_resource.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Dict, Optional, Sequence - - -def list_data_to_dicts(list_data: Sequence): - return list(map(lambda x: x.dict(), list_data)) - - -def list_response_of( - data=[Dict], before: Optional[str] = None, after: Optional[str] = None -): - return { - "object": "list", - "data": data, - "list_metadata": {"before": before, "after": after}, - } diff --git a/tests/utils/syncify.py b/tests/utils/syncify.py deleted file mode 100644 index 4fd4e0e2..00000000 --- a/tests/utils/syncify.py +++ /dev/null @@ -1,20 +0,0 @@ -import asyncio -from typing import Any, Coroutine, TypeVar, Union - -ReturnType = TypeVar("ReturnType") - - -def _call(coro: Coroutine[Any, Any, ReturnType]) -> ReturnType: - try: - loop = asyncio.get_running_loop() - except RuntimeError: - return asyncio.run(coro) - else: - return loop.run_until_complete(coro) - - -def syncify(obj: Union[Coroutine[Any, Any, ReturnType], ReturnType]) -> ReturnType: - if isinstance(obj, Coroutine): - return _call(obj) - - return obj diff --git a/tests/utils/test_request_helper.py b/tests/utils/test_request_helper.py deleted file mode 100644 index 9afd2d54..00000000 --- a/tests/utils/test_request_helper.py +++ /dev/null @@ -1,12 +0,0 @@ -from workos.utils.request_helper import RequestHelper - - -class TestRequestHelper: - def test_build_parameterized_url(self): - assert RequestHelper.build_parameterized_url("a/b/c") == "a/b/c" - assert RequestHelper.build_parameterized_url("a/{b}/c", b="b") == "a/b/c" - assert RequestHelper.build_parameterized_url("a/{b}/c", b="test") == "a/test/c" - assert ( - RequestHelper.build_parameterized_url("a/{b}/c", b="i/am/being/sneaky") - == "a/i/am/being/sneaky/c" - ) From 1516214b1d79cd8ceb1b92882abd24b437ae08e5 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 31 Mar 2026 10:13:42 -0400 Subject: [PATCH 10/26] remove pydantic --- pyproject.toml | 1 - uv.lock | 156 ------------------------------------------------- 2 files changed, 157 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5d48e242..776ea504 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,6 @@ requires-python = ">=3.10" dependencies = [ "cryptography~=46.0", "httpx~=0.28", - "pydantic~=2.12", "pyjwt~=2.12", ] diff --git a/uv.lock b/uv.lock index 966ad92b..27de01ef 100644 --- a/uv.lock +++ b/uv.lock @@ -2,15 +2,6 @@ version = 1 revision = 3 requires-python = ">=3.10" -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, -] - [[package]] name = "anyio" version = "4.11.0" @@ -512,139 +503,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, ] -[[package]] -name = "pydantic" -version = "2.12.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.41.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, - { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, - { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, - { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, - { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, - { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, - { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, - { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, - { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, - { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, - { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, - { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, - { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, - { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -830,18 +688,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] -[[package]] -name = "typing-inspection" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, -] - [[package]] name = "virtualenv" version = "20.36.1" @@ -864,7 +710,6 @@ source = { editable = "." } dependencies = [ { name = "cryptography" }, { name = "httpx" }, - { name = "pydantic" }, { name = "pyjwt" }, ] @@ -900,7 +745,6 @@ type-check = [ requires-dist = [ { name = "cryptography", specifier = "~=46.0" }, { name = "httpx", specifier = "~=0.28" }, - { name = "pydantic", specifier = "~=2.12" }, { name = "pyjwt", specifier = "~=2.12" }, ] From fda2f5e074b193df997ff383c8f28642a7b89d36 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 31 Mar 2026 11:03:12 -0400 Subject: [PATCH 11/26] Rewrite CLAUDE.md --- CLAUDE.md | 81 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index da138c01..9493d11b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,6 +2,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +## Code Generation + +Most of the SDK is **auto-generated** by `oagen` (an internal OpenAPI code generator). Generated files begin with `# This file is auto-generated by oagen. Do not edit.` Hand-maintained code is fenced with `@oagen-ignore-start` / `@oagen-ignore-end` markers (e.g., `session.py`, `passwordless.py`, `vault.py`). + ## Development Commands ### Installation and Setup @@ -16,7 +20,7 @@ uv sync --locked --dev # Install package in development mode with dev dependenci uv run ruff format . # Format code uv run ruff format --check . # Check formatting without making changes uv run ruff check . # Lint code -uv run mypy # Type checking +uv run pyright # Type checking ``` ### Testing @@ -69,50 +73,61 @@ bash scripts/build_and_upload_dist.sh # Build and upload to PyPI The SDK provides both synchronous and asynchronous clients: -- `WorkOSClient` (sync) and `AsyncWorkOSClient` (async) are the main entry points -- Both inherit from `BaseClient` which handles configuration and module initialization -- Each feature area (SSO, Directory Sync, etc.) has dedicated module classes -- HTTP clients (`SyncHTTPClient`/`AsyncHTTPClient`) handle the actual API communication +- `WorkOS` (sync) and `AsyncWorkOS` (async) are the main entry points (exported from `workos/__init__.py`) +- Both inherit from `_BaseWorkOS` (in `workos/_client.py`) which handles configuration, HTTP transport, retry logic, and error mapping +- Each feature area (SSO, Organizations, etc.) is exposed as a `@functools.cached_property` on the client for lazy loading +- Complex feature areas use namespace classes (e.g., `UserManagementNamespace`, `OrganizationsNamespace`) to group sub-resources +- Backward-compatible aliases exist: `WorkOSClient = WorkOS`, `AsyncWorkOSClient = AsyncWorkOS` +- HTTP transport uses `httpx` directly; there is no separate HTTP client abstraction layer ### Module Structure -Each WorkOS feature has its own module following this pattern: +Each feature module follows this layout: + +``` +src/workos/{module_name}/ + __init__.py # Re-exports from _resource and models + _resource.py # Sync and Async resource classes (e.g., SSO + AsyncSSO) + models/ + __init__.py # Re-exports all model classes + {model}.py # Individual dataclass model files +``` -- **Module class** (e.g., `SSO`) - main API interface -- **Types directory** (e.g., `workos/types/sso/`) - Pydantic models for API objects -- **Tests** (e.g., `tests/test_sso.py`) - comprehensive test coverage +Resource classes take a `WorkOS` or `AsyncWorkOS` client reference and call `self._client.request()` or `self._client.request_page()` for paginated endpoints. ### Type System -- All models inherit from `WorkOSModel` (extends Pydantic `BaseModel`) -- Strict typing with mypy enforcement (`strict = True` in mypy.ini) -- Support for both sync and async operations via `SyncOrAsync` typing +- All models use `@dataclass(slots=True)` — **not** Pydantic +- Each model implements `from_dict(cls, data) -> Self` for deserialization and `to_dict() -> Dict` for serialization +- The `Deserializable` protocol in `workos/_types.py` defines the `from_dict` contract +- `RequestOptions` (a `TypedDict`) allows per-call overrides for headers, timeout, retries, etc. +- Type checking uses **pyright** (configured in `pyrightconfig.json`) -### Testing Framework +### Pagination -- Uses pytest with custom fixtures for mocking HTTP clients -- `@pytest.mark.sync_and_async()` decorator runs tests for both sync/async variants -- Comprehensive fixtures in `conftest.py` for HTTP mocking and pagination testing -- Test utilities in `tests/utils/` for common patterns +- `SyncPage[T]` and `AsyncPage[T]` dataclasses in `workos/_pagination.py` represent paginated results +- Cursor-based: `before`/`after` properties, `has_more()` check +- `auto_paging_iter()` transparently fetches subsequent pages +- Backward-compatible alias: `WorkOSListResource = SyncPage` -### HTTP Client Abstraction +### Error Handling -- Base HTTP client (`_BaseHTTPClient`) with sync/async implementations -- Request helper utilities for consistent API interaction patterns -- Built-in pagination support with `WorkOSListResource` type -- Automatic retry and error handling +All exceptions live in `workos/_errors.py` and inherit from `WorkOSError`: -### Key Patterns +- `BadRequestError` (400), `AuthenticationError` (401), `ForbiddenError` (403), `NotFoundError` (404), `ConflictError` (409), `UnprocessableEntityError` (422), `RateLimitExceededError` (429), `ServerError` (5xx) +- `ConfigurationError`, `WorkOSConnectionError`, `WorkOSTimeoutError` for non-HTTP errors +- `STATUS_CODE_TO_ERROR` dict maps status codes to exception classes + +### Testing Framework -- **Dual client support**: Every module supports both sync and async operations -- **Type safety**: Extensive use of Pydantic models and strict mypy checking -- **Pagination**: Consistent cursor-based pagination across list endpoints -- **Error handling**: Custom exception classes in `workos/exceptions.py` -- **Configuration**: Environment variable support (`WORKOS_API_KEY`, `WORKOS_CLIENT_ID`) +- Uses **pytest** with **pytest-httpx** for HTTP mocking (provides the `httpx_mock` fixture) +- Separate test classes for sync (`TestSSO`) and async (`TestAsyncSSO`) variants +- Async tests use `@pytest.mark.asyncio` +- JSON fixtures in `tests/fixtures/`, loaded via `load_fixture()` from `tests/generated_helpers.py` +- Client fixtures `workos` and `async_workos` defined in `tests/conftest.py` -When adding new features: +### Configuration -1. Create module class with both sync/async HTTP client support -2. Add Pydantic models in appropriate `types/` subdirectory -3. Implement comprehensive tests using the sync_and_async marker -4. Follow existing patterns for pagination, error handling, and type annotations +- Environment variable support: `WORKOS_API_KEY`, `WORKOS_CLIENT_ID` +- Retry logic: exponential backoff with jitter, retries on 429/5xx, respects `Retry-After` headers +- Default timeout: 30 seconds From 567c6066e64a17ce509cfbd8db521b0c267c12bf Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 31 Mar 2026 12:23:42 -0400 Subject: [PATCH 12/26] lock down `oagen-ignore` files --- src/workos/_client.py | 1126 ++++++++++++++++++++++++++ src/workos/passwordless.py | 169 ++++ src/workos/session.py | 462 +++++++++++ src/workos/vault.py | 605 ++++++++++++++ src/workos/webhooks/_verification.py | 94 +++ 5 files changed, 2456 insertions(+) create mode 100644 src/workos/_client.py create mode 100644 src/workos/passwordless.py create mode 100644 src/workos/session.py create mode 100644 src/workos/vault.py create mode 100644 src/workos/webhooks/_verification.py diff --git a/src/workos/_client.py b/src/workos/_client.py new file mode 100644 index 00000000..f7ff3341 --- /dev/null +++ b/src/workos/_client.py @@ -0,0 +1,1126 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +import asyncio +import functools +import os +import platform +import time +import uuid +import random +from datetime import datetime, timezone +from email.utils import parsedate_to_datetime +from typing import Any, Dict, Optional, Type, cast, overload + +import httpx + +from ._errors import ( + BaseRequestException, + RateLimitExceededException, + ServerException, + WorkOSConnectionException, + WorkOSTimeoutException, + STATUS_CODE_TO_EXCEPTION, +) +from ._pagination import AsyncPage, SyncPage +from ._types import D, Deserializable, RequestOptions +from .api_keys._resource import ApiKeys, AsyncApiKeys +from .multi_factor_auth.challenges._resource import ( + MultiFactorAuthChallenges, + AsyncMultiFactorAuthChallenges, +) +from .multi_factor_auth._resource import MultiFactorAuth, AsyncMultiFactorAuth +from .workos_connect._resource import WorkosConnect, AsyncWorkosConnect +from .authorization._resource import Authorization, AsyncAuthorization +from .permissions._resource import Permissions, AsyncPermissions +from .applications._resource import Applications, AsyncApplications +from .application_client_secrets._resource import ( + ApplicationClientSecrets, + AsyncApplicationClientSecrets, +) +from .connections._resource import Connections, AsyncConnections +from .pipes._resource import Pipes, AsyncPipes +from .directories._resource import Directories, AsyncDirectories +from .directory_groups._resource import DirectoryGroups, AsyncDirectoryGroups +from .directory_users._resource import DirectoryUsers, AsyncDirectoryUsers +from .events._resource import Events, AsyncEvents +from .feature_flags._resource import FeatureFlags, AsyncFeatureFlags +from .feature_flags.targets._resource import ( + FeatureFlagsTargets, + AsyncFeatureFlagsTargets, +) +from .organization_domains._resource import ( + OrganizationDomains, + AsyncOrganizationDomains, +) +from .organizations._resource import Organizations, AsyncOrganizations +from .organizations.api_keys._resource import ( + OrganizationsApiKeys, + AsyncOrganizationsApiKeys, +) +from .organizations.feature_flags._resource import ( + OrganizationsFeatureFlags, + AsyncOrganizationsFeatureFlags, +) +from .admin_portal._resource import AdminPortal, AsyncAdminPortal +from .radar._resource import Radar, AsyncRadar +from .sso._resource import SSO, AsyncSSO +from .user_management.session_tokens._resource import ( + UserManagementSessionTokens, + AsyncUserManagementSessionTokens, +) +from .user_management.authentication._resource import ( + UserManagementAuthentication, + AsyncUserManagementAuthentication, +) +from .user_management.cors_origins._resource import ( + UserManagementCorsOrigins, + AsyncUserManagementCorsOrigins, +) +from .user_management.users._resource import ( + UserManagementUsers, + AsyncUserManagementUsers, +) +from .user_management.invitations._resource import ( + UserManagementInvitations, + AsyncUserManagementInvitations, +) +from .user_management.jwt_template._resource import ( + UserManagementJWTTemplate, + AsyncUserManagementJWTTemplate, +) +from .user_management.magic_auth._resource import ( + UserManagementMagicAuth, + AsyncUserManagementMagicAuth, +) +from .user_management.organization_membership._resource import ( + UserManagementOrganizationMembership, + AsyncUserManagementOrganizationMembership, +) +from .user_management.redirect_uris._resource import ( + UserManagementRedirectUris, + AsyncUserManagementRedirectUris, +) +from .user_management_users.feature_flags._resource import ( + UserManagementUsersFeatureFlags, + AsyncUserManagementUsersFeatureFlags, +) +from .user_management_users.authorized_applications._resource import ( + UserManagementUsersAuthorizedApplications, + AsyncUserManagementUsersAuthorizedApplications, +) +from .user_management.data_providers._resource import ( + UserManagementDataProviders, + AsyncUserManagementDataProviders, +) +from .user_management.multi_factor_authentication._resource import ( + UserManagementMultiFactorAuthentication, + AsyncUserManagementMultiFactorAuthentication, +) +from .webhooks._resource import Webhooks, AsyncWebhooks +from .widgets._resource import Widgets, AsyncWidgets +from .audit_logs._resource import AuditLogs, AsyncAuditLogs + +try: + from importlib.metadata import version as _pkg_version + + VERSION = _pkg_version("workos") +except Exception: + VERSION = "0.0.0" + +RETRY_STATUS_CODES = {429, 500, 502, 503, 504} +MAX_RETRIES = 3 +INITIAL_RETRY_DELAY = 0.5 +MAX_RETRY_DELAY = 8.0 +RETRY_MULTIPLIER = 2.0 + + +class MultiFactorAuthNamespace(MultiFactorAuth): + """MultiFactorAuth resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + super().__init__(client) + + @functools.cached_property + def challenges(self) -> MultiFactorAuthChallenges: + return MultiFactorAuthChallenges(self._client) + + +class FeatureFlagsNamespace(FeatureFlags): + """FeatureFlags resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + super().__init__(client) + + @functools.cached_property + def targets(self) -> FeatureFlagsTargets: + return FeatureFlagsTargets(self._client) + + +class OrganizationsNamespace(Organizations): + """Organizations resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + super().__init__(client) + + @functools.cached_property + def api_keys(self) -> OrganizationsApiKeys: + return OrganizationsApiKeys(self._client) + + @functools.cached_property + def feature_flags(self) -> OrganizationsFeatureFlags: + return OrganizationsFeatureFlags(self._client) + + +class UserManagementNamespace(object): + """UserManagement resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + @functools.cached_property + def session_tokens(self) -> UserManagementSessionTokens: + return UserManagementSessionTokens(self._client) + + @functools.cached_property + def authentication(self) -> UserManagementAuthentication: + return UserManagementAuthentication(self._client) + + @functools.cached_property + def cors_origins(self) -> UserManagementCorsOrigins: + return UserManagementCorsOrigins(self._client) + + @functools.cached_property + def users(self) -> UserManagementUsers: + return UserManagementUsers(self._client) + + @functools.cached_property + def invitations(self) -> UserManagementInvitations: + return UserManagementInvitations(self._client) + + @functools.cached_property + def jwt_template(self) -> UserManagementJWTTemplate: + return UserManagementJWTTemplate(self._client) + + @functools.cached_property + def magic_auth(self) -> UserManagementMagicAuth: + return UserManagementMagicAuth(self._client) + + @functools.cached_property + def organization_membership(self) -> UserManagementOrganizationMembership: + return UserManagementOrganizationMembership(self._client) + + @functools.cached_property + def redirect_uris(self) -> UserManagementRedirectUris: + return UserManagementRedirectUris(self._client) + + @functools.cached_property + def data_providers(self) -> UserManagementDataProviders: + return UserManagementDataProviders(self._client) + + @functools.cached_property + def multi_factor_authentication(self) -> UserManagementMultiFactorAuthentication: + return UserManagementMultiFactorAuthentication(self._client) + + +class UserManagementUsersNamespace(object): + """UserManagementUsers resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + @functools.cached_property + def feature_flags(self) -> UserManagementUsersFeatureFlags: + return UserManagementUsersFeatureFlags(self._client) + + @functools.cached_property + def authorized_applications(self) -> UserManagementUsersAuthorizedApplications: + return UserManagementUsersAuthorizedApplications(self._client) + + +class AsyncMultiFactorAuthNamespace(AsyncMultiFactorAuth): + """MultiFactorAuth resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + super().__init__(client) + + @functools.cached_property + def challenges(self) -> AsyncMultiFactorAuthChallenges: + return AsyncMultiFactorAuthChallenges(self._client) + + +class AsyncFeatureFlagsNamespace(AsyncFeatureFlags): + """FeatureFlags resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + super().__init__(client) + + @functools.cached_property + def targets(self) -> AsyncFeatureFlagsTargets: + return AsyncFeatureFlagsTargets(self._client) + + +class AsyncOrganizationsNamespace(AsyncOrganizations): + """Organizations resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + super().__init__(client) + + @functools.cached_property + def api_keys(self) -> AsyncOrganizationsApiKeys: + return AsyncOrganizationsApiKeys(self._client) + + @functools.cached_property + def feature_flags(self) -> AsyncOrganizationsFeatureFlags: + return AsyncOrganizationsFeatureFlags(self._client) + + +class AsyncUserManagementNamespace(object): + """UserManagement resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + @functools.cached_property + def session_tokens(self) -> AsyncUserManagementSessionTokens: + return AsyncUserManagementSessionTokens(self._client) + + @functools.cached_property + def authentication(self) -> AsyncUserManagementAuthentication: + return AsyncUserManagementAuthentication(self._client) + + @functools.cached_property + def cors_origins(self) -> AsyncUserManagementCorsOrigins: + return AsyncUserManagementCorsOrigins(self._client) + + @functools.cached_property + def users(self) -> AsyncUserManagementUsers: + return AsyncUserManagementUsers(self._client) + + @functools.cached_property + def invitations(self) -> AsyncUserManagementInvitations: + return AsyncUserManagementInvitations(self._client) + + @functools.cached_property + def jwt_template(self) -> AsyncUserManagementJWTTemplate: + return AsyncUserManagementJWTTemplate(self._client) + + @functools.cached_property + def magic_auth(self) -> AsyncUserManagementMagicAuth: + return AsyncUserManagementMagicAuth(self._client) + + @functools.cached_property + def organization_membership(self) -> AsyncUserManagementOrganizationMembership: + return AsyncUserManagementOrganizationMembership(self._client) + + @functools.cached_property + def redirect_uris(self) -> AsyncUserManagementRedirectUris: + return AsyncUserManagementRedirectUris(self._client) + + @functools.cached_property + def data_providers(self) -> AsyncUserManagementDataProviders: + return AsyncUserManagementDataProviders(self._client) + + @functools.cached_property + def multi_factor_authentication( + self, + ) -> AsyncUserManagementMultiFactorAuthentication: + return AsyncUserManagementMultiFactorAuthentication(self._client) + + +class AsyncUserManagementUsersNamespace(object): + """UserManagementUsers resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + @functools.cached_property + def feature_flags(self) -> AsyncUserManagementUsersFeatureFlags: + return AsyncUserManagementUsersFeatureFlags(self._client) + + @functools.cached_property + def authorized_applications(self) -> AsyncUserManagementUsersAuthorizedApplications: + return AsyncUserManagementUsersAuthorizedApplications(self._client) + + +class _BaseWorkOSClient: + """Shared WorkOS client implementation.""" + + def __init__( + self, + *, + api_key: Optional[str] = None, + client_id: Optional[str] = None, + base_url: str = "https://api.workos.com", + request_timeout: int = 25, + max_retries: int = MAX_RETRIES, + ) -> None: + self._api_key = api_key or os.environ.get("WORKOS_API_KEY") + if not self._api_key: + raise ValueError( + "WorkOS API key must be provided when instantiating the client " + "or via the WORKOS_API_KEY environment variable." + ) + self.client_id = client_id or os.environ.get("WORKOS_CLIENT_ID") + if not self.client_id: + raise ValueError( + "WorkOS client ID must be provided when instantiating the client " + "or via the WORKOS_CLIENT_ID environment variable." + ) + # Ensure base_url has a trailing slash for backward compatibility + self._base_url = base_url.rstrip("/") + "/" + self._request_timeout = request_timeout + self._max_retries = max_retries + self._jwt_leeway: float = 0.0 + + @property + def base_url(self) -> str: + """The base URL for API requests.""" + return self._base_url + + def build_url(self, path: str, params: Optional[Dict[str, Any]] = None) -> str: + """Build a full URL with query parameters for redirect/authorization endpoints.""" + from urllib.parse import urlencode + + base = self._base_url.rstrip("/") + url = f"{base}/{path}" + if params: + url = f"{url}?{urlencode(params)}" + return url + + @staticmethod + def _parse_retry_after(retry_after: Optional[str]) -> Optional[float]: + """Parse Retry-After as seconds or an HTTP-date.""" + if not retry_after: + return None + value = retry_after.strip() + if not value: + return None + try: + return max(float(value), 0.0) + except ValueError: + pass + try: + retry_at = parsedate_to_datetime(value) + except (TypeError, ValueError, IndexError, OverflowError): + return None + if retry_at.tzinfo is None: + retry_at = retry_at.replace(tzinfo=timezone.utc) + return max((retry_at - datetime.now(timezone.utc)).total_seconds(), 0.0) + + @staticmethod + def _calculate_retry_delay( + attempt: int, retry_after: Optional[str] = None + ) -> float: + """Calculate retry delay with exponential backoff and jitter.""" + parsed_retry_after = _BaseWorkOSClient._parse_retry_after(retry_after) + if parsed_retry_after is not None: + return parsed_retry_after + delay = min(INITIAL_RETRY_DELAY * (RETRY_MULTIPLIER**attempt), MAX_RETRY_DELAY) + return delay * (0.5 + random.random()) + + def _resolve_base_url(self, request_options: Optional[RequestOptions]) -> str: + if request_options: + base_url = request_options.get("base_url") + if base_url: + return str(base_url).rstrip("/") + return self._base_url.rstrip("/") + + def _resolve_timeout(self, request_options: Optional[RequestOptions]) -> float: + timeout = self._request_timeout + if request_options: + t = request_options.get("timeout") + if isinstance(t, (int, float)): + timeout = float(t) + return timeout + + def _resolve_max_retries(self, request_options: Optional[RequestOptions]) -> int: + if request_options: + retries = request_options.get("max_retries") + if isinstance(retries, int): + return retries + return self._max_retries + + def _build_headers( + self, + method: str, + idempotency_key: Optional[str], + request_options: Optional[RequestOptions], + ) -> Dict[str, str]: + headers: Dict[str, str] = { + "Authorization": f"Bearer {self._api_key}", + "Content-Type": "application/json", + "User-Agent": f"workos-python/{VERSION} python/{platform.python_version()}", + } + effective_idempotency_key = idempotency_key + if effective_idempotency_key is None and request_options: + request_option_idempotency_key = request_options.get("idempotency_key") + if isinstance(request_option_idempotency_key, str): + effective_idempotency_key = request_option_idempotency_key + if effective_idempotency_key is None and method.lower() == "post": + effective_idempotency_key = str(uuid.uuid4()) + if effective_idempotency_key: + headers["Idempotency-Key"] = effective_idempotency_key + if request_options: + extra = request_options.get("extra_headers") + if isinstance(extra, dict): + headers.update(cast(Dict[str, str], extra)) + return headers + + def _deserialize_response( + self, response: httpx.Response, model: Optional[Type[Deserializable]] + ) -> Any: + if response.status_code == 204 or not response.content: + return None + data: Dict[str, Any] = cast(Dict[str, Any], response.json()) + if model is not None: + return model.from_dict(data) + return data + + @staticmethod + def _raise_error(response: httpx.Response) -> None: + """Raise an appropriate error based on the response status code.""" + request_id = response.headers.get("x-request-id", "") + raw_body = response.text + request = response.request + request_url = str(request.url) if request is not None else None + request_method = request.method if request is not None else None + try: + body: Dict[str, Any] = response.json() + message: str = str(body.get("message", response.text)) + code: Optional[str] = str(body["code"]) if "code" in body else None + param = cast(Optional[str], body.get("param")) + except Exception: + message = response.text + code = None + param = None + + error_class = STATUS_CODE_TO_EXCEPTION.get(response.status_code) + if error_class: + if error_class is RateLimitExceededException: + retry_after = _BaseWorkOSClient._parse_retry_after( + response.headers.get("Retry-After") + ) + raise RateLimitExceededException( + message, + retry_after=retry_after, + request_id=request_id, + code=code, + param=param, + raw_body=raw_body, + request_url=request_url, + request_method=request_method, + ) + raise error_class( + message, + request_id=request_id, + code=code, + param=param, + raw_body=raw_body, + request_url=request_url, + request_method=request_method, + ) + + if response.status_code >= 500: + raise ServerException( + message, + status_code=response.status_code, + request_id=request_id, + code=code, + param=param, + raw_body=raw_body, + request_url=request_url, + request_method=request_method, + ) + + raise BaseRequestException( + message, + status_code=response.status_code, + request_id=request_id, + code=code, + param=param, + raw_body=raw_body, + request_url=request_url, + request_method=request_method, + ) + + +class WorkOSClient(_BaseWorkOSClient): + """Synchronous WorkOS API client.""" + + def __init__( + self, + *, + api_key: Optional[str] = None, + client_id: Optional[str] = None, + base_url: str = "https://api.workos.com", + request_timeout: int = 25, + max_retries: int = MAX_RETRIES, + ) -> None: + """Initialize the WorkOS client. + + Args: + api_key: WorkOS API key. Falls back to the WORKOS_API_KEY environment variable. + client_id: WorkOS client ID. Falls back to the WORKOS_CLIENT_ID environment variable. + base_url: Base URL for API requests. Defaults to "https://api.workos.com". + request_timeout: HTTP request timeout in seconds. Defaults to 25. + max_retries: Maximum number of retries for failed requests. Defaults to 3. + + Raises: + ValueError: If api_key is not provided and WORKOS_API_KEY is not set. + """ + super().__init__( + api_key=api_key, + client_id=client_id, + base_url=base_url, + request_timeout=request_timeout, + max_retries=max_retries, + ) + self._client = httpx.Client(timeout=request_timeout, follow_redirects=True) + + def close(self) -> None: + """Close the underlying HTTP client and release resources.""" + self._client.close() + + def __enter__(self) -> "WorkOSClient": + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self.close() + + @functools.cached_property + def api_keys(self) -> ApiKeys: + return ApiKeys(self) + + @functools.cached_property + def workos_connect(self) -> WorkosConnect: + return WorkosConnect(self) + + @functools.cached_property + def authorization(self) -> Authorization: + return Authorization(self) + + @functools.cached_property + def permissions(self) -> Permissions: + return Permissions(self) + + @functools.cached_property + def applications(self) -> Applications: + return Applications(self) + + @functools.cached_property + def application_client_secrets(self) -> ApplicationClientSecrets: + return ApplicationClientSecrets(self) + + @functools.cached_property + def connections(self) -> Connections: + return Connections(self) + + @functools.cached_property + def pipes(self) -> Pipes: + return Pipes(self) + + @functools.cached_property + def directories(self) -> Directories: + return Directories(self) + + @functools.cached_property + def directory_groups(self) -> DirectoryGroups: + return DirectoryGroups(self) + + @functools.cached_property + def directory_users(self) -> DirectoryUsers: + return DirectoryUsers(self) + + @functools.cached_property + def events(self) -> Events: + return Events(self) + + @functools.cached_property + def organization_domains(self) -> OrganizationDomains: + return OrganizationDomains(self) + + @functools.cached_property + def admin_portal(self) -> AdminPortal: + return AdminPortal(self) + + @functools.cached_property + def radar(self) -> Radar: + return Radar(self) + + @functools.cached_property + def sso(self) -> SSO: + return SSO(self) + + @functools.cached_property + def webhooks(self) -> Webhooks: + return Webhooks(self) + + @functools.cached_property + def widgets(self) -> Widgets: + return Widgets(self) + + @functools.cached_property + def audit_logs(self) -> AuditLogs: + return AuditLogs(self) + + @functools.cached_property + def multi_factor_auth(self) -> MultiFactorAuthNamespace: + return MultiFactorAuthNamespace(self) + + @functools.cached_property + def feature_flags(self) -> FeatureFlagsNamespace: + return FeatureFlagsNamespace(self) + + @functools.cached_property + def organizations(self) -> OrganizationsNamespace: + return OrganizationsNamespace(self) + + @functools.cached_property + def user_management(self) -> UserManagementNamespace: + return UserManagementNamespace(self) + + @functools.cached_property + def user_management_users(self) -> UserManagementUsersNamespace: + return UserManagementUsersNamespace(self) + + @functools.cached_property + def connect(self) -> Any: + return self.workos_connect + + @functools.cached_property + def directory_sync(self) -> Any: + return self.directories + + @functools.cached_property + def fga(self) -> Any: + return self.authorization + + @functools.cached_property + def mfa(self) -> Any: + return self.multi_factor_auth + + @functools.cached_property + def passwordless(self) -> Any: + return object() # Backward-compatible stub + + @functools.cached_property + def portal(self) -> Any: + return self.admin_portal + + @functools.cached_property + def vault(self) -> Any: + return object() # Backward-compatible stub + + @overload + def request( + self, + method: str, + path: str, + *, + model: Type[D], + params: Optional[Dict[str, Any]] = ..., + body: Optional[Dict[str, Any]] = ..., + idempotency_key: Optional[str] = ..., + request_options: Optional[RequestOptions] = ..., + ) -> D: ... + + @overload + def request( + self, + method: str, + path: str, + *, + model: None = ..., + params: Optional[Dict[str, Any]] = ..., + body: Optional[Dict[str, Any]] = ..., + idempotency_key: Optional[str] = ..., + request_options: Optional[RequestOptions] = ..., + ) -> Optional[Dict[str, Any]]: ... + + def request( + self, + method: str, + path: str, + *, + params: Optional[Dict[str, Any]] = None, + body: Optional[Dict[str, Any]] = None, + model: Optional[Type[Deserializable]] = None, + idempotency_key: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> Any: + """Make an HTTP request with retry logic.""" + url = f"{self._resolve_base_url(request_options)}/{path}" + headers = self._build_headers(method, idempotency_key, request_options) + timeout = self._resolve_timeout(request_options) + max_retries = self._resolve_max_retries(request_options) + last_error: Optional[Exception] = None + for attempt in range(max_retries + 1): + try: + response = self._client.request( + method=method.upper(), + url=url, + params=params, + json=body if body is not None else None, + headers=headers, + timeout=timeout, + ) + if response.status_code in RETRY_STATUS_CODES and attempt < max_retries: + delay = self._calculate_retry_delay( + attempt, response.headers.get("Retry-After") + ) + time.sleep(delay) + continue + if response.status_code >= 400: + self._raise_error(response) + return self._deserialize_response(response, model) + except httpx.TimeoutException as e: + last_error = e + if attempt < max_retries: + time.sleep(self._calculate_retry_delay(attempt)) + continue + raise WorkOSTimeoutException(f"Request timed out: {e}") from e + except httpx.ConnectError as e: + last_error = e + if attempt < max_retries: + time.sleep(self._calculate_retry_delay(attempt)) + continue + raise WorkOSConnectionException(f"Connection failed: {e}") from e + except httpx.HTTPError as e: + last_error = e + if attempt < max_retries: + time.sleep(self._calculate_retry_delay(attempt)) + continue + raise BaseRequestException(f"Network error: {e}") from e + raise BaseRequestException("Max retries exceeded") from last_error + + def request_page( + self, + method: str, + path: str, + *, + model: Type[D], + params: Optional[Dict[str, Any]] = None, + body: Optional[Dict[str, Any]] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[D]: + """Make an HTTP request that returns a paginated response.""" + raw = self.request( + method=method, + path=path, + params=params, + body=body, + request_options=request_options, + ) + data: Dict[str, Any] = raw if isinstance(raw, dict) else {} + raw_items: list[Any] = cast(list[Any], data.get("data") or []) + items: list[D] = [ + cast(D, model.from_dict(cast(Dict[str, Any], item))) for item in raw_items + ] + list_metadata: Dict[str, Any] = cast( + Dict[str, Any], data.get("list_metadata", {}) + ) + + def _fetch(*, after: Optional[str] = None) -> SyncPage[D]: + next_params = {**(params or {}), "after": after} + return self.request_page( + method=method, + path=path, + model=model, + params=next_params, + body=body, + request_options=request_options, + ) + + return SyncPage(data=items, list_metadata=list_metadata, _fetch_page=_fetch) + + +class AsyncWorkOSClient(_BaseWorkOSClient): + """Asynchronous WorkOS API client.""" + + def __init__( + self, + *, + api_key: Optional[str] = None, + client_id: Optional[str] = None, + base_url: str = "https://api.workos.com", + request_timeout: int = 25, + max_retries: int = MAX_RETRIES, + ) -> None: + """Initialize the async WorkOS client. + + Args: + api_key: WorkOS API key. Falls back to the WORKOS_API_KEY environment variable. + client_id: WorkOS client ID. Falls back to the WORKOS_CLIENT_ID environment variable. + base_url: Base URL for API requests. Defaults to "https://api.workos.com". + request_timeout: HTTP request timeout in seconds. Defaults to 25. + max_retries: Maximum number of retries for failed requests. Defaults to 3. + + Raises: + ValueError: If api_key is not provided and WORKOS_API_KEY is not set. + """ + super().__init__( + api_key=api_key, + client_id=client_id, + base_url=base_url, + request_timeout=request_timeout, + max_retries=max_retries, + ) + self._client = httpx.AsyncClient(timeout=request_timeout, follow_redirects=True) + + async def close(self) -> None: + """Close the underlying HTTP client and release resources.""" + await self._client.aclose() + + async def __aenter__(self) -> "AsyncWorkOSClient": + return self + + async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + await self.close() + + @functools.cached_property + def api_keys(self) -> AsyncApiKeys: + return AsyncApiKeys(self) + + @functools.cached_property + def workos_connect(self) -> AsyncWorkosConnect: + return AsyncWorkosConnect(self) + + @functools.cached_property + def authorization(self) -> AsyncAuthorization: + return AsyncAuthorization(self) + + @functools.cached_property + def permissions(self) -> AsyncPermissions: + return AsyncPermissions(self) + + @functools.cached_property + def applications(self) -> AsyncApplications: + return AsyncApplications(self) + + @functools.cached_property + def application_client_secrets(self) -> AsyncApplicationClientSecrets: + return AsyncApplicationClientSecrets(self) + + @functools.cached_property + def connections(self) -> AsyncConnections: + return AsyncConnections(self) + + @functools.cached_property + def pipes(self) -> AsyncPipes: + return AsyncPipes(self) + + @functools.cached_property + def directories(self) -> AsyncDirectories: + return AsyncDirectories(self) + + @functools.cached_property + def directory_groups(self) -> AsyncDirectoryGroups: + return AsyncDirectoryGroups(self) + + @functools.cached_property + def directory_users(self) -> AsyncDirectoryUsers: + return AsyncDirectoryUsers(self) + + @functools.cached_property + def events(self) -> AsyncEvents: + return AsyncEvents(self) + + @functools.cached_property + def organization_domains(self) -> AsyncOrganizationDomains: + return AsyncOrganizationDomains(self) + + @functools.cached_property + def admin_portal(self) -> AsyncAdminPortal: + return AsyncAdminPortal(self) + + @functools.cached_property + def radar(self) -> AsyncRadar: + return AsyncRadar(self) + + @functools.cached_property + def sso(self) -> AsyncSSO: + return AsyncSSO(self) + + @functools.cached_property + def webhooks(self) -> AsyncWebhooks: + return AsyncWebhooks(self) + + @functools.cached_property + def widgets(self) -> AsyncWidgets: + return AsyncWidgets(self) + + @functools.cached_property + def audit_logs(self) -> AsyncAuditLogs: + return AsyncAuditLogs(self) + + @functools.cached_property + def multi_factor_auth(self) -> AsyncMultiFactorAuthNamespace: + return AsyncMultiFactorAuthNamespace(self) + + @functools.cached_property + def feature_flags(self) -> AsyncFeatureFlagsNamespace: + return AsyncFeatureFlagsNamespace(self) + + @functools.cached_property + def organizations(self) -> AsyncOrganizationsNamespace: + return AsyncOrganizationsNamespace(self) + + @functools.cached_property + def user_management(self) -> AsyncUserManagementNamespace: + return AsyncUserManagementNamespace(self) + + @functools.cached_property + def user_management_users(self) -> AsyncUserManagementUsersNamespace: + return AsyncUserManagementUsersNamespace(self) + + @functools.cached_property + def connect(self) -> Any: + return self.workos_connect + + @functools.cached_property + def directory_sync(self) -> Any: + return self.directories + + @functools.cached_property + def fga(self) -> Any: + return self.authorization + + @functools.cached_property + def mfa(self) -> Any: + return self.multi_factor_auth + + @functools.cached_property + def passwordless(self) -> Any: + return object() # Backward-compatible stub + + @functools.cached_property + def portal(self) -> Any: + return self.admin_portal + + @functools.cached_property + def vault(self) -> Any: + return object() # Backward-compatible stub + + @overload + async def request( + self, + method: str, + path: str, + *, + model: Type[D], + params: Optional[Dict[str, Any]] = ..., + body: Optional[Dict[str, Any]] = ..., + idempotency_key: Optional[str] = ..., + request_options: Optional[RequestOptions] = ..., + ) -> D: ... + + @overload + async def request( + self, + method: str, + path: str, + *, + model: None = ..., + params: Optional[Dict[str, Any]] = ..., + body: Optional[Dict[str, Any]] = ..., + idempotency_key: Optional[str] = ..., + request_options: Optional[RequestOptions] = ..., + ) -> Optional[Dict[str, Any]]: ... + + async def request( + self, + method: str, + path: str, + *, + params: Optional[Dict[str, Any]] = None, + body: Optional[Dict[str, Any]] = None, + model: Optional[Type[Deserializable]] = None, + idempotency_key: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> Any: + """Make an async HTTP request with retry logic.""" + url = f"{self._resolve_base_url(request_options)}/{path}" + headers = self._build_headers(method, idempotency_key, request_options) + timeout = self._resolve_timeout(request_options) + max_retries = self._resolve_max_retries(request_options) + last_error: Optional[Exception] = None + for attempt in range(max_retries + 1): + try: + response = await self._client.request( + method=method.upper(), + url=url, + params=params, + json=body if body is not None else None, + headers=headers, + timeout=timeout, + ) + if response.status_code in RETRY_STATUS_CODES and attempt < max_retries: + delay = self._calculate_retry_delay( + attempt, response.headers.get("Retry-After") + ) + await asyncio.sleep(delay) + continue + if response.status_code >= 400: + self._raise_error(response) + return self._deserialize_response(response, model) + except httpx.TimeoutException as e: + last_error = e + if attempt < max_retries: + await asyncio.sleep(self._calculate_retry_delay(attempt)) + continue + raise WorkOSTimeoutException(f"Request timed out: {e}") from e + except httpx.ConnectError as e: + last_error = e + if attempt < max_retries: + await asyncio.sleep(self._calculate_retry_delay(attempt)) + continue + raise WorkOSConnectionException(f"Connection failed: {e}") from e + except httpx.HTTPError as e: + last_error = e + if attempt < max_retries: + await asyncio.sleep(self._calculate_retry_delay(attempt)) + continue + raise BaseRequestException(f"Network error: {e}") from e + raise BaseRequestException("Max retries exceeded") from last_error + + async def request_page( + self, + method: str, + path: str, + *, + model: Type[D], + params: Optional[Dict[str, Any]] = None, + body: Optional[Dict[str, Any]] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[D]: + """Make an async HTTP request that returns a paginated response.""" + raw = await self.request( + method=method, + path=path, + params=params, + body=body, + request_options=request_options, + ) + data: Dict[str, Any] = raw if isinstance(raw, dict) else {} + raw_items: list[Any] = cast(list[Any], data.get("data") or []) + items: list[D] = [ + cast(D, model.from_dict(cast(Dict[str, Any], item))) for item in raw_items + ] + list_metadata: Dict[str, Any] = cast( + Dict[str, Any], data.get("list_metadata", {}) + ) + + async def _fetch(*, after: Optional[str] = None) -> AsyncPage[D]: + next_params = {**(params or {}), "after": after} + return await self.request_page( + method=method, + path=path, + model=model, + params=next_params, + body=body, + request_options=request_options, + ) + + return AsyncPage(data=items, list_metadata=list_metadata, _fetch_page=_fetch) diff --git a/src/workos/passwordless.py b/src/workos/passwordless.py new file mode 100644 index 00000000..88667944 --- /dev/null +++ b/src/workos/passwordless.py @@ -0,0 +1,169 @@ +# @oagen-ignore-file +# This file is hand-maintained. The passwordless API endpoints are not yet in +# the OpenAPI spec, so this module provides the functionality until they are. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Dict, Literal, Optional + +if TYPE_CHECKING: + from ._client import AsyncWorkOS, WorkOS + +PasswordlessSessionType = Literal["MagicLink"] + + +@dataclass(slots=True) +class PasswordlessSession: + """Representation of a WorkOS Passwordless Session Response.""" + + object: Literal["passwordless_session"] + id: str + email: str + expires_at: str + link: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "PasswordlessSession": + return cls( + object=data.get("object", "passwordless_session"), + id=data["id"], + email=data["email"], + expires_at=data["expires_at"], + link=data["link"], + ) + + def to_dict(self) -> Dict[str, Any]: + return { + "object": self.object, + "id": self.id, + "email": self.email, + "expires_at": self.expires_at, + "link": self.link, + } + + +class Passwordless: + """Offers methods through the WorkOS Passwordless service.""" + + def __init__(self, client: "WorkOS") -> None: + self._client = client + + def create_session( + self, + *, + email: str, + type: PasswordlessSessionType, + redirect_uri: Optional[str] = None, + state: Optional[str] = None, + expires_in: Optional[int] = None, + ) -> PasswordlessSession: + """Create a Passwordless Session. + + Args: + email: The email of the user to authenticate. + type: The type of Passwordless Session ('MagicLink'). + redirect_uri: The redirect endpoint for the callback from WorkOS. (Optional) + state: Arbitrary state to pass through the redirect. (Optional) + expires_in: Seconds until expiry (900–86400). (Optional) + + Returns: + PasswordlessSession + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "email": email, + "type": type, + "redirect_uri": redirect_uri, + "state": state, + "expires_in": expires_in, + }.items() + if v is not None + } + + response = self._client.request( + method="post", + path="passwordless/sessions", + body=body, + model=PasswordlessSession, + ) + return response + + def send_session(self, session_id: str) -> Literal[True]: + """Send a Passwordless Session via email. + + Args: + session_id: The unique identifier of the Passwordless Session. + + Returns: + True on success. + """ + self._client.request( + method="post", + path=f"passwordless/sessions/{session_id}/send", + ) + return True + + +class AsyncPasswordless: + """Async variant of the WorkOS Passwordless service.""" + + def __init__(self, client: "AsyncWorkOS") -> None: + self._client = client + + async def create_session( + self, + *, + email: str, + type: PasswordlessSessionType, + redirect_uri: Optional[str] = None, + state: Optional[str] = None, + expires_in: Optional[int] = None, + ) -> PasswordlessSession: + """Create a Passwordless Session. + + Args: + email: The email of the user to authenticate. + type: The type of Passwordless Session ('MagicLink'). + redirect_uri: The redirect endpoint for the callback from WorkOS. (Optional) + state: Arbitrary state to pass through the redirect. (Optional) + expires_in: Seconds until expiry (900–86400). (Optional) + + Returns: + PasswordlessSession + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "email": email, + "type": type, + "redirect_uri": redirect_uri, + "state": state, + "expires_in": expires_in, + }.items() + if v is not None + } + + response = await self._client.request( + method="post", + path="passwordless/sessions", + body=body, + model=PasswordlessSession, + ) + return response + + async def send_session(self, session_id: str) -> Literal[True]: + """Send a Passwordless Session via email. + + Args: + session_id: The unique identifier of the Passwordless Session. + + Returns: + True on success. + """ + await self._client.request( + method="post", + path=f"passwordless/sessions/{session_id}/send", + ) + return True diff --git a/src/workos/session.py b/src/workos/session.py new file mode 100644 index 00000000..6cc9d8e9 --- /dev/null +++ b/src/workos/session.py @@ -0,0 +1,462 @@ +# @oagen-ignore-file +# This file is hand-maintained. Session management (sealed cookies, JWT +# validation, JWKS) is client-side logic that cannot be generated from the +# OpenAPI spec. + +from __future__ import annotations + +import json +from dataclasses import dataclass +from enum import Enum +from functools import lru_cache +from typing import ( + TYPE_CHECKING, + Any, + Dict, + List, + Optional, + Sequence, + Union, + cast, +) + +import jwt +from cryptography.fernet import Fernet +from jwt import PyJWKClient + +if TYPE_CHECKING: + from ._client import AsyncWorkOS, WorkOS + + +# --------------------------------------------------------------------------- +# Types +# --------------------------------------------------------------------------- + + +class AuthenticateWithSessionCookieFailureReason(Enum): + INVALID_JWT = "invalid_jwt" + INVALID_SESSION_COOKIE = "invalid_session_cookie" + NO_SESSION_COOKIE_PROVIDED = "no_session_cookie_provided" + + +@dataclass(slots=True) +class AuthenticateWithSessionCookieSuccessResponse: + authenticated: bool # Always True + session_id: str + organization_id: Optional[str] = None + role: Optional[str] = None + roles: Optional[Sequence[str]] = None + permissions: Optional[Sequence[str]] = None + user: Optional[Dict[str, Any]] = None + impersonator: Optional[Dict[str, Any]] = None + entitlements: Optional[Sequence[str]] = None + feature_flags: Optional[Sequence[str]] = None + + +@dataclass(slots=True) +class AuthenticateWithSessionCookieErrorResponse: + authenticated: bool # Always False + reason: Union[AuthenticateWithSessionCookieFailureReason, str] + + +@dataclass(slots=True) +class RefreshWithSessionCookieSuccessResponse: + authenticated: bool # Always True + sealed_session: str + session_id: str + organization_id: Optional[str] = None + role: Optional[str] = None + roles: Optional[Sequence[str]] = None + permissions: Optional[Sequence[str]] = None + user: Optional[Dict[str, Any]] = None + impersonator: Optional[Dict[str, Any]] = None + entitlements: Optional[Sequence[str]] = None + feature_flags: Optional[Sequence[str]] = None + + +@dataclass(slots=True) +class RefreshWithSessionCookieErrorResponse: + authenticated: bool # Always False + reason: Union[AuthenticateWithSessionCookieFailureReason, str] + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +@lru_cache(maxsize=None) +def _get_jwks_client(jwks_url: str) -> PyJWKClient: + return PyJWKClient(jwks_url) + + +def seal_data(data: Dict[str, Any], key: str) -> str: + """Encrypt a dictionary with Fernet symmetric encryption.""" + fernet = Fernet(key) + encrypted_bytes = fernet.encrypt(json.dumps(data).encode()) + return encrypted_bytes.decode("utf-8") + + +def unseal_data(sealed_data: str, key: str) -> Dict[str, Any]: + """Decrypt a Fernet-encrypted string back to a dictionary.""" + fernet = Fernet(key) + encrypted_bytes = sealed_data.encode("utf-8") + decrypted_str = fernet.decrypt(encrypted_bytes).decode() + return cast(Dict[str, Any], json.loads(decrypted_str)) + + +# --------------------------------------------------------------------------- +# Session (sync) +# --------------------------------------------------------------------------- + + +class Session: + """Server-side session management using sealed cookies and JWT validation.""" + + _JWK_ALGORITHMS: List[str] = ["RS256"] + + def __init__( + self, + *, + client: "WorkOS", + session_data: str, + cookie_password: str, + ) -> None: + if not cookie_password: + raise ValueError("cookie_password is required") + + self._client = client + self.session_data = session_data + self.cookie_password = cookie_password + + jwks_url = f"{client.base_url}sso/jwks/{client.client_id}" + self.jwks = _get_jwks_client(jwks_url) + + def authenticate( + self, + ) -> Union[ + AuthenticateWithSessionCookieSuccessResponse, + AuthenticateWithSessionCookieErrorResponse, + ]: + """Validate the sealed session cookie and return the session claims.""" + if not self.session_data: + return AuthenticateWithSessionCookieErrorResponse( + authenticated=False, + reason=AuthenticateWithSessionCookieFailureReason.NO_SESSION_COOKIE_PROVIDED, + ) + + try: + session = unseal_data(self.session_data, self.cookie_password) + except Exception: + return AuthenticateWithSessionCookieErrorResponse( + authenticated=False, + reason=AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE, + ) + + if not session.get("access_token"): + return AuthenticateWithSessionCookieErrorResponse( + authenticated=False, + reason=AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE, + ) + + try: + signing_key = self.jwks.get_signing_key_from_jwt(session["access_token"]) + decoded = jwt.decode( + session["access_token"], + signing_key.key, + algorithms=self._JWK_ALGORITHMS, + options={"verify_aud": False}, + leeway=self._client._jwt_leeway, + ) + except jwt.exceptions.InvalidTokenError: + return AuthenticateWithSessionCookieErrorResponse( + authenticated=False, + reason=AuthenticateWithSessionCookieFailureReason.INVALID_JWT, + ) + + return AuthenticateWithSessionCookieSuccessResponse( + authenticated=True, + session_id=decoded["sid"], + organization_id=decoded.get("org_id"), + role=decoded.get("role"), + roles=decoded.get("roles"), + permissions=decoded.get("permissions"), + entitlements=decoded.get("entitlements"), + user=session.get("user"), + impersonator=session.get("impersonator"), + feature_flags=decoded.get("feature_flags"), + ) + + def refresh( + self, + *, + organization_id: Optional[str] = None, + cookie_password: Optional[str] = None, + ) -> Union[ + RefreshWithSessionCookieSuccessResponse, + RefreshWithSessionCookieErrorResponse, + ]: + """Refresh the session using the stored refresh token.""" + effective_cookie_password = cookie_password or self.cookie_password + + try: + session = unseal_data(self.session_data, effective_cookie_password) + except Exception: + return RefreshWithSessionCookieErrorResponse( + authenticated=False, + reason=AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE, + ) + + if not session.get("refresh_token") or not session.get("user"): + return RefreshWithSessionCookieErrorResponse( + authenticated=False, + reason=AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE, + ) + + try: + # Use raw dict request because the generated AuthenticateResponse + # doesn't include sealed_session, and the request body needs the + # session parameter which isn't in the generated request models. + body: Dict[str, Any] = { + "grant_type": "refresh_token", + "client_id": self._client.client_id, + "client_secret": self._client._api_key, + "refresh_token": session["refresh_token"], + "session": { + "seal_session": True, + "cookie_password": effective_cookie_password, + }, + } + if organization_id is not None: + body["organization_id"] = organization_id + + auth_response: Dict[str, Any] = self._client.request( # type: ignore[assignment] + method="post", + path="user_management/authenticate", + body=body, + ) + + self.session_data = str(auth_response["sealed_session"]) + self.cookie_password = effective_cookie_password + + signing_key = self.jwks.get_signing_key_from_jwt( + auth_response["access_token"] + ) + decoded = jwt.decode( + auth_response["access_token"], + signing_key.key, + algorithms=self._JWK_ALGORITHMS, + options={"verify_aud": False}, + leeway=self._client._jwt_leeway, + ) + + return RefreshWithSessionCookieSuccessResponse( + authenticated=True, + sealed_session=str(auth_response["sealed_session"]), + session_id=decoded["sid"], + organization_id=decoded.get("org_id"), + role=decoded.get("role"), + roles=decoded.get("roles"), + permissions=decoded.get("permissions"), + entitlements=decoded.get("entitlements"), + user=auth_response.get("user"), + impersonator=auth_response.get("impersonator"), + feature_flags=decoded.get("feature_flags"), + ) + except Exception as e: + return RefreshWithSessionCookieErrorResponse( + authenticated=False, reason=str(e) + ) + + def get_logout_url(self, return_to: Optional[str] = None) -> str: + """Get the logout URL for the current session.""" + auth_response = self.authenticate() + + if isinstance(auth_response, AuthenticateWithSessionCookieErrorResponse): + raise ValueError( + f"Failed to extract session ID for logout URL: {auth_response.reason}" + ) + + return self._client.user_management.authentication.logout( + session_id=auth_response.session_id, + return_to=return_to, + ) + + +# --------------------------------------------------------------------------- +# AsyncSession +# --------------------------------------------------------------------------- + + +class AsyncSession: + """Async server-side session management using sealed cookies and JWT validation.""" + + _JWK_ALGORITHMS: List[str] = ["RS256"] + + def __init__( + self, + *, + client: "AsyncWorkOS", + session_data: str, + cookie_password: str, + ) -> None: + if not cookie_password: + raise ValueError("cookie_password is required") + + self._client = client + self.session_data = session_data + self.cookie_password = cookie_password + + jwks_url = f"{client.base_url}sso/jwks/{client.client_id}" + self.jwks = _get_jwks_client(jwks_url) + + def authenticate( + self, + ) -> Union[ + AuthenticateWithSessionCookieSuccessResponse, + AuthenticateWithSessionCookieErrorResponse, + ]: + """Validate the sealed session cookie and return the session claims. + + Note: This method is synchronous because it only performs local + operations (Fernet decryption and JWT validation using cached JWKS). + """ + if not self.session_data: + return AuthenticateWithSessionCookieErrorResponse( + authenticated=False, + reason=AuthenticateWithSessionCookieFailureReason.NO_SESSION_COOKIE_PROVIDED, + ) + + try: + session = unseal_data(self.session_data, self.cookie_password) + except Exception: + return AuthenticateWithSessionCookieErrorResponse( + authenticated=False, + reason=AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE, + ) + + if not session.get("access_token"): + return AuthenticateWithSessionCookieErrorResponse( + authenticated=False, + reason=AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE, + ) + + try: + signing_key = self.jwks.get_signing_key_from_jwt(session["access_token"]) + decoded = jwt.decode( + session["access_token"], + signing_key.key, + algorithms=self._JWK_ALGORITHMS, + options={"verify_aud": False}, + leeway=self._client._jwt_leeway, + ) + except jwt.exceptions.InvalidTokenError: + return AuthenticateWithSessionCookieErrorResponse( + authenticated=False, + reason=AuthenticateWithSessionCookieFailureReason.INVALID_JWT, + ) + + return AuthenticateWithSessionCookieSuccessResponse( + authenticated=True, + session_id=decoded["sid"], + organization_id=decoded.get("org_id"), + role=decoded.get("role"), + roles=decoded.get("roles"), + permissions=decoded.get("permissions"), + entitlements=decoded.get("entitlements"), + user=session.get("user"), + impersonator=session.get("impersonator"), + feature_flags=decoded.get("feature_flags"), + ) + + async def refresh( + self, + *, + organization_id: Optional[str] = None, + cookie_password: Optional[str] = None, + ) -> Union[ + RefreshWithSessionCookieSuccessResponse, + RefreshWithSessionCookieErrorResponse, + ]: + """Refresh the session using the stored refresh token.""" + effective_cookie_password = cookie_password or self.cookie_password + + try: + session = unseal_data(self.session_data, effective_cookie_password) + except Exception: + return RefreshWithSessionCookieErrorResponse( + authenticated=False, + reason=AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE, + ) + + if not session.get("refresh_token") or not session.get("user"): + return RefreshWithSessionCookieErrorResponse( + authenticated=False, + reason=AuthenticateWithSessionCookieFailureReason.INVALID_SESSION_COOKIE, + ) + + try: + body: Dict[str, Any] = { + "grant_type": "refresh_token", + "client_id": self._client.client_id, + "client_secret": self._client._api_key, + "refresh_token": session["refresh_token"], + "session": { + "seal_session": True, + "cookie_password": effective_cookie_password, + }, + } + if organization_id is not None: + body["organization_id"] = organization_id + + auth_response: Dict[str, Any] = await self._client.request( # type: ignore[assignment] + method="post", + path="user_management/authenticate", + body=body, + ) + + self.session_data = str(auth_response["sealed_session"]) + self.cookie_password = effective_cookie_password + + signing_key = self.jwks.get_signing_key_from_jwt( + auth_response["access_token"] + ) + decoded = jwt.decode( + auth_response["access_token"], + signing_key.key, + algorithms=self._JWK_ALGORITHMS, + options={"verify_aud": False}, + leeway=self._client._jwt_leeway, + ) + + return RefreshWithSessionCookieSuccessResponse( + authenticated=True, + sealed_session=str(auth_response["sealed_session"]), + session_id=decoded["sid"], + organization_id=decoded.get("org_id"), + role=decoded.get("role"), + roles=decoded.get("roles"), + permissions=decoded.get("permissions"), + entitlements=decoded.get("entitlements"), + user=auth_response.get("user"), + impersonator=auth_response.get("impersonator"), + feature_flags=decoded.get("feature_flags"), + ) + except Exception as e: + return RefreshWithSessionCookieErrorResponse( + authenticated=False, reason=str(e) + ) + + async def get_logout_url(self, return_to: Optional[str] = None) -> str: + """Get the logout URL for the current session.""" + auth_response = self.authenticate() + + if isinstance(auth_response, AuthenticateWithSessionCookieErrorResponse): + raise ValueError( + f"Failed to extract session ID for logout URL: {auth_response.reason}" + ) + + return await self._client.user_management.authentication.logout( + session_id=auth_response.session_id, + return_to=return_to, + ) diff --git a/src/workos/vault.py b/src/workos/vault.py new file mode 100644 index 00000000..7a2dea84 --- /dev/null +++ b/src/workos/vault.py @@ -0,0 +1,605 @@ +# @oagen-ignore-file +# This file is hand-maintained. The vault API endpoints are not yet in the +# OpenAPI spec, so this module provides the functionality until they are. +# The encrypt/decrypt methods use client-side AES-GCM and will always be +# hand-maintained regardless of spec coverage. + +from __future__ import annotations + +import base64 +import os +from dataclasses import dataclass +from typing import ( + TYPE_CHECKING, + Any, + Dict, + List, + Optional, + Sequence, + Tuple, +) + +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.backends import default_backend + +if TYPE_CHECKING: + from ._client import AsyncWorkOS, WorkOS + +# --------------------------------------------------------------------------- +# Types +# --------------------------------------------------------------------------- + +KeyContext = Dict[str, str] + + +@dataclass(slots=True) +class DataKey: + id: str + key: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "DataKey": + return cls(id=data["id"], key=data["key"]) + + def to_dict(self) -> Dict[str, Any]: + return {"id": self.id, "key": self.key} + + +@dataclass(slots=True) +class DataKeyPair: + context: KeyContext + data_key: DataKey + encrypted_keys: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "DataKeyPair": + return cls( + context=data["context"], + data_key=DataKey.from_dict(data["data_key"]), + encrypted_keys=data["encrypted_keys"], + ) + + def to_dict(self) -> Dict[str, Any]: + return { + "context": self.context, + "data_key": self.data_key.to_dict(), + "encrypted_keys": self.encrypted_keys, + } + + +@dataclass(slots=True) +class DecodedKeys: + iv: bytes + tag: bytes + keys: str # Base64-encoded string + ciphertext: bytes + + +@dataclass(slots=True) +class ObjectUpdateBy: + id: str + name: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ObjectUpdateBy": + return cls(id=data["id"], name=data["name"]) + + def to_dict(self) -> Dict[str, Any]: + return {"id": self.id, "name": self.name} + + +@dataclass(slots=True) +class ObjectMetadata: + context: KeyContext + environment_id: str + id: str + key_id: str + updated_at: str + updated_by: ObjectUpdateBy + version_id: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ObjectMetadata": + return cls( + context=data["context"], + environment_id=data["environment_id"], + id=data["id"], + key_id=data["key_id"], + updated_at=data["updated_at"], + updated_by=ObjectUpdateBy.from_dict(data["updated_by"]), + version_id=data["version_id"], + ) + + def to_dict(self) -> Dict[str, Any]: + return { + "context": self.context, + "environment_id": self.environment_id, + "id": self.id, + "key_id": self.key_id, + "updated_at": self.updated_at, + "updated_by": self.updated_by.to_dict(), + "version_id": self.version_id, + } + + +@dataclass(slots=True) +class VaultObject: + id: str + metadata: ObjectMetadata + name: str + value: Optional[str] = None + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "VaultObject": + return cls( + id=data["id"], + metadata=ObjectMetadata.from_dict(data["metadata"]), + name=data["name"], + value=data.get("value"), + ) + + def to_dict(self) -> Dict[str, Any]: + result: Dict[str, Any] = { + "id": self.id, + "metadata": self.metadata.to_dict(), + "name": self.name, + } + if self.value is not None: + result["value"] = self.value + return result + + +@dataclass(slots=True) +class ObjectDigest: + id: str + name: str + updated_at: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ObjectDigest": + return cls(id=data["id"], name=data["name"], updated_at=data["updated_at"]) + + def to_dict(self) -> Dict[str, Any]: + return {"id": self.id, "name": self.name, "updated_at": self.updated_at} + + +@dataclass(slots=True) +class ObjectVersion: + created_at: str + current_version: bool + id: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ObjectVersion": + return cls( + created_at=data["created_at"], + current_version=data["current_version"], + id=data["id"], + ) + + def to_dict(self) -> Dict[str, Any]: + return { + "created_at": self.created_at, + "current_version": self.current_version, + "id": self.id, + } + + +# --------------------------------------------------------------------------- +# Crypto helpers (AES-GCM, LEB128) +# --------------------------------------------------------------------------- + + +def _aes_gcm_encrypt( + plaintext: bytes, key: bytes, iv: bytes, aad: Optional[bytes] +) -> Dict[str, bytes]: + encryptor = Cipher( + algorithms.AES(key), modes.GCM(iv), backend=default_backend() + ).encryptor() + if aad: + encryptor.authenticate_additional_data(aad) + ciphertext = encryptor.update(plaintext) + encryptor.finalize() + return {"ciphertext": ciphertext, "iv": iv, "tag": encryptor.tag} + + +def _aes_gcm_decrypt( + ciphertext: bytes, + key: bytes, + iv: bytes, + tag: bytes, + aad: Optional[bytes] = None, +) -> bytes: + decryptor = Cipher( + algorithms.AES(key), modes.GCM(iv, tag), backend=default_backend() + ).decryptor() + if aad: + decryptor.authenticate_additional_data(aad) + return decryptor.update(ciphertext) + decryptor.finalize() + + +def _encode_u32_leb128(value: int) -> bytes: + """Encode a 32-bit unsigned integer as LEB128.""" + if value < 0 or value > 0xFFFFFFFF: + raise ValueError("Value must be a 32-bit unsigned integer") + encoded = bytearray() + while True: + byte = value & 0x7F + value >>= 7 + if value != 0: + byte |= 0x80 + encoded.append(byte) + if value == 0: + break + return bytes(encoded) + + +def _decode_u32_leb128(buf: bytes) -> Tuple[int, int]: + """Decode an unsigned LEB128-encoded 32-bit integer. Returns (value, bytes_consumed).""" + res = 0 + bit = 0 + for i, b in enumerate(buf): + if i > 4: + raise ValueError("LEB128 integer overflow (was more than 4 bytes)") + res |= (b & 0x7F) << (7 * bit) + if (b & 0x80) == 0: + return res, i + 1 + bit += 1 + raise ValueError("LEB128 integer not found") + + +def _decode_encrypted_payload(encrypted_data_b64: str) -> DecodedKeys: + """Extract IV, tag, keyBlobLength, keyBlob, and ciphertext from a base64 payload. + + Format: [IV:12b][TAG:16b][LEB128 Length][keyBlob][ciphertext] + """ + try: + payload = base64.b64decode(encrypted_data_b64) + except Exception as e: + raise ValueError("Base64 decoding failed") from e + + iv = payload[0:12] + tag = payload[12:28] + key_len, leb_len = _decode_u32_leb128(payload[28:]) + keys_index = 28 + leb_len + keys_end = keys_index + key_len + keys_slice = payload[keys_index:keys_end] + keys = base64.b64encode(keys_slice).decode("utf-8") + ciphertext = payload[keys_end:] + return DecodedKeys(iv=iv, tag=tag, keys=keys, ciphertext=ciphertext) + + +DEFAULT_RESPONSE_LIMIT = 10 + + +# --------------------------------------------------------------------------- +# Sync Vault +# --------------------------------------------------------------------------- + + +class Vault: + """WorkOS Vault service — encryption, key management, and secret storage.""" + + def __init__(self, client: "WorkOS") -> None: + self._client = client + + # -- KV operations -- + + def read_object(self, *, object_id: str) -> VaultObject: + """Get a Vault object with the value decrypted.""" + response = self._client.request( + method="get", + path=f"vault/v1/kv/{object_id}", + model=VaultObject, + ) + return response + + def read_object_by_name(self, *, name: str) -> VaultObject: + """Get a Vault object by name with the value decrypted.""" + response = self._client.request( + method="get", + path=f"vault/v1/kv/name/{name}", + model=VaultObject, + ) + return response + + def list_objects( + self, + *, + limit: int = DEFAULT_RESPONSE_LIMIT, + before: Optional[str] = None, + after: Optional[str] = None, + ) -> Dict[str, Any]: + """Gets a list of encrypted Vault objects.""" + params: Dict[str, Any] = {"limit": limit} + if before is not None: + params["before"] = before + if after is not None: + params["after"] = after + + response = self._client.request( + method="get", + path="vault/v1/kv", + params=params, + ) + return response # type: ignore[return-value] + + def list_object_versions(self, *, object_id: str) -> Sequence[ObjectVersion]: + """Gets a list of versions for a specific Vault object.""" + response = self._client.request( + method="get", + path=f"vault/v1/kv/{object_id}/versions", + ) + data: List[Dict[str, Any]] = (response or {}).get("data", []) # type: ignore[union-attr] + return [ObjectVersion.from_dict(v) for v in data] + + def create_object( + self, + *, + name: str, + value: str, + key_context: KeyContext, + ) -> ObjectMetadata: + """Create a new Vault encrypted object.""" + response = self._client.request( + method="post", + path="vault/v1/kv", + body={"name": name, "value": value, "key_context": key_context}, + model=ObjectMetadata, + ) + return response + + def update_object( + self, + *, + object_id: str, + value: str, + version_check: Optional[str] = None, + ) -> VaultObject: + """Update an existing Vault object.""" + body: Dict[str, Any] = {"value": value} + if version_check is not None: + body["version_check"] = version_check + + response = self._client.request( + method="put", + path=f"vault/v1/kv/{object_id}", + body=body, + model=VaultObject, + ) + return response + + def delete_object(self, *, object_id: str) -> None: + """Permanently delete a Vault encrypted object.""" + self._client.request( + method="delete", + path=f"vault/v1/kv/{object_id}", + ) + + # -- Key operations -- + + def create_data_key(self, *, key_context: KeyContext) -> DataKeyPair: + """Generate a data key for local encryption.""" + response: Dict[str, Any] = self._client.request( # type: ignore[assignment] + method="post", + path="vault/v1/keys/data-key", + body={"context": key_context}, + ) + return DataKeyPair( + context=response["context"], + data_key=DataKey(id=response["id"], key=response["data_key"]), + encrypted_keys=response["encrypted_keys"], + ) + + def decrypt_data_key(self, *, keys: str) -> DataKey: + """Decrypt encrypted data keys previously generated by create_data_key.""" + response: Dict[str, Any] = self._client.request( # type: ignore[assignment] + method="post", + path="vault/v1/keys/decrypt", + body={"keys": keys}, + ) + return DataKey(id=response["id"], key=response["data_key"]) + + # -- Client-side encryption -- + + def encrypt( + self, + *, + data: str, + key_context: KeyContext, + associated_data: Optional[str] = None, + ) -> str: + """Encrypt data locally using AES-GCM with a data key derived from the context.""" + key_pair = self.create_data_key(key_context=key_context) + + key = base64.b64decode(key_pair.data_key.key) + key_blob = base64.b64decode(key_pair.encrypted_keys) + prefix_len_buffer = _encode_u32_leb128(len(key_blob)) + aad_buffer = associated_data.encode("utf-8") if associated_data else None + iv = os.urandom(12) + + result = _aes_gcm_encrypt(data.encode("utf-8"), key, iv, aad_buffer) + + combined = ( + result["iv"] + + result["tag"] + + prefix_len_buffer + + key_blob + + result["ciphertext"] + ) + return base64.b64encode(combined).decode("utf-8") + + def decrypt( + self, *, encrypted_data: str, associated_data: Optional[str] = None + ) -> str: + """Decrypt data that was previously encrypted using the encrypt method.""" + decoded = _decode_encrypted_payload(encrypted_data) + data_key = self.decrypt_data_key(keys=decoded.keys) + + key = base64.b64decode(data_key.key) + aad_buffer = associated_data.encode("utf-8") if associated_data else None + + decrypted_bytes = _aes_gcm_decrypt( + ciphertext=decoded.ciphertext, + key=key, + iv=decoded.iv, + tag=decoded.tag, + aad=aad_buffer, + ) + return decrypted_bytes.decode("utf-8") + + +# --------------------------------------------------------------------------- +# Async Vault +# --------------------------------------------------------------------------- + + +class AsyncVault: + """Async WorkOS Vault service.""" + + def __init__(self, client: "AsyncWorkOS") -> None: + self._client = client + + async def read_object(self, *, object_id: str) -> VaultObject: + response = await self._client.request( + method="get", + path=f"vault/v1/kv/{object_id}", + model=VaultObject, + ) + return response + + async def read_object_by_name(self, *, name: str) -> VaultObject: + response = await self._client.request( + method="get", + path=f"vault/v1/kv/name/{name}", + model=VaultObject, + ) + return response + + async def list_objects( + self, + *, + limit: int = DEFAULT_RESPONSE_LIMIT, + before: Optional[str] = None, + after: Optional[str] = None, + ) -> Dict[str, Any]: + params: Dict[str, Any] = {"limit": limit} + if before is not None: + params["before"] = before + if after is not None: + params["after"] = after + response = await self._client.request( + method="get", + path="vault/v1/kv", + params=params, + ) + return response # type: ignore[return-value] + + async def list_object_versions(self, *, object_id: str) -> Sequence[ObjectVersion]: + response = await self._client.request( + method="get", + path=f"vault/v1/kv/{object_id}/versions", + ) + data: List[Dict[str, Any]] = (response or {}).get("data", []) # type: ignore[union-attr] + return [ObjectVersion.from_dict(v) for v in data] + + async def create_object( + self, + *, + name: str, + value: str, + key_context: KeyContext, + ) -> ObjectMetadata: + response = await self._client.request( + method="post", + path="vault/v1/kv", + body={"name": name, "value": value, "key_context": key_context}, + model=ObjectMetadata, + ) + return response + + async def update_object( + self, + *, + object_id: str, + value: str, + version_check: Optional[str] = None, + ) -> VaultObject: + body: Dict[str, Any] = {"value": value} + if version_check is not None: + body["version_check"] = version_check + response = await self._client.request( + method="put", + path=f"vault/v1/kv/{object_id}", + body=body, + model=VaultObject, + ) + return response + + async def delete_object(self, *, object_id: str) -> None: + await self._client.request( + method="delete", + path=f"vault/v1/kv/{object_id}", + ) + + async def create_data_key(self, *, key_context: KeyContext) -> DataKeyPair: + response: Dict[str, Any] = await self._client.request( # type: ignore[assignment] + method="post", + path="vault/v1/keys/data-key", + body={"context": key_context}, + ) + return DataKeyPair( + context=response["context"], + data_key=DataKey(id=response["id"], key=response["data_key"]), + encrypted_keys=response["encrypted_keys"], + ) + + async def decrypt_data_key(self, *, keys: str) -> DataKey: + response: Dict[str, Any] = await self._client.request( # type: ignore[assignment] + method="post", + path="vault/v1/keys/decrypt", + body={"keys": keys}, + ) + return DataKey(id=response["id"], key=response["data_key"]) + + async def encrypt( + self, + *, + data: str, + key_context: KeyContext, + associated_data: Optional[str] = None, + ) -> str: + key_pair = await self.create_data_key(key_context=key_context) + + key = base64.b64decode(key_pair.data_key.key) + key_blob = base64.b64decode(key_pair.encrypted_keys) + prefix_len_buffer = _encode_u32_leb128(len(key_blob)) + aad_buffer = associated_data.encode("utf-8") if associated_data else None + iv = os.urandom(12) + + result = _aes_gcm_encrypt(data.encode("utf-8"), key, iv, aad_buffer) + combined = ( + result["iv"] + + result["tag"] + + prefix_len_buffer + + key_blob + + result["ciphertext"] + ) + return base64.b64encode(combined).decode("utf-8") + + async def decrypt( + self, *, encrypted_data: str, associated_data: Optional[str] = None + ) -> str: + decoded = _decode_encrypted_payload(encrypted_data) + data_key = await self.decrypt_data_key(keys=decoded.keys) + + key = base64.b64decode(data_key.key) + aad_buffer = associated_data.encode("utf-8") if associated_data else None + + decrypted_bytes = _aes_gcm_decrypt( + ciphertext=decoded.ciphertext, + key=key, + iv=decoded.iv, + tag=decoded.tag, + aad=aad_buffer, + ) + return decrypted_bytes.decode("utf-8") diff --git a/src/workos/webhooks/_verification.py b/src/workos/webhooks/_verification.py new file mode 100644 index 00000000..abcbf494 --- /dev/null +++ b/src/workos/webhooks/_verification.py @@ -0,0 +1,94 @@ +# @oagen-ignore-file +# This file is hand-maintained. It provides webhook signature verification +# utilities that complement the auto-generated webhook CRUD operations. + +from __future__ import annotations + +import hashlib +import hmac +import json +import time +from typing import Any, Dict, Optional, Union + +WebhookPayload = Union[bytes, bytearray] + +DEFAULT_TOLERANCE = 180 # seconds + + +def verify_event( + *, + event_body: WebhookPayload, + event_signature: str, + secret: str, + tolerance: Optional[int] = DEFAULT_TOLERANCE, +) -> Dict[str, Any]: + """Verify and deserialize the signature of a Webhook event. + + Args: + event_body: The Webhook body (bytes). + event_signature: The signature from the 'WorkOS-Signature' header. + secret: The secret for the webhook endpoint (from the WorkOS dashboard). + tolerance: The number of seconds the Webhook event is valid for. (Optional) + + Returns: + The deserialized webhook event as a dictionary. + + Raises: + ValueError: If the signature cannot be verified or the timestamp is out of range. + """ + verify_header( + event_body=event_body, + event_signature=event_signature, + secret=secret, + tolerance=tolerance, + ) + return json.loads(event_body) + + +def verify_header( + *, + event_body: WebhookPayload, + event_signature: str, + secret: str, + tolerance: Optional[int] = None, +) -> None: + """Verify the signature of a Webhook. Raises ValueError if verification fails. + + Args: + event_body: The Webhook body (bytes). + event_signature: The signature from the 'WorkOS-Signature' header. + secret: The secret for the webhook endpoint (from the WorkOS dashboard). + tolerance: The number of seconds the Webhook event is valid for. (Optional) + + Raises: + ValueError: If the signature cannot be verified or the timestamp is out of range. + """ + try: + issued_timestamp, signature_hash = event_signature.split(", ") + except (ValueError, AttributeError): + raise ValueError( + "Unable to extract timestamp and signature hash from header", + event_signature, + ) + + issued_timestamp = issued_timestamp[2:] + signature_hash = signature_hash[3:] + max_seconds_since_issued = tolerance or DEFAULT_TOLERANCE + current_time = time.time() + timestamp_in_seconds = int(issued_timestamp) / 1000 + seconds_since_issued = current_time - timestamp_in_seconds + + if seconds_since_issued > max_seconds_since_issued: + raise ValueError("Timestamp outside the tolerance zone") + + unhashed_string = "{0}.{1}".format(issued_timestamp, event_body.decode("utf-8")) + expected_signature = hmac.new( + secret.encode("utf-8"), + unhashed_string.encode("utf-8"), + digestmod=hashlib.sha256, + ).hexdigest() + + if not hmac.compare_digest(signature_hash, expected_signature): + raise ValueError( + "Signature hash does not match the expected signature hash for payload" + ) From 603f86085aa8ca43520203f461f06cf9aa66af3d Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 31 Mar 2026 13:18:08 -0400 Subject: [PATCH 13/26] Add generated SDK modules with hand-maintained fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add all auto-generated resource modules, models, types, and tests - Add verify_event/verify_header back to Webhooks as @oagen-ignore methods - Add list_connections/get_connection/delete_connection to SSO as @oagen-ignore methods - Rename organizations update_organization→update, delete_organization→delete - Fix docstring exception names (*Error→*Exception) across all resource files Co-Authored-By: Claude Opus 4.6 (1M context) --- src/workos/__init__.py | 63 +- src/workos/_client.py | 525 +- src/workos/_errors.py | 314 ++ src/workos/_pagination.py | 107 + src/workos/_types.py | 26 + src/workos/admin_portal/__init__.py | 4 + src/workos/admin_portal/_resource.py | 152 + src/workos/admin_portal/models/__init__.py | 6 + .../admin_portal/models/generate_link.py | 69 + .../admin_portal/models/intent_options.py | 36 + .../models/portal_link_response.py | 33 + .../admin_portal/models/sso_intent_options.py | 39 + src/workos/api_keys/__init__.py | 4 + src/workos/api_keys/_resource.py | 149 + src/workos/api_keys/models/__init__.py | 8 + src/workos/api_keys/models/api_key.py | 80 + src/workos/api_keys/models/api_key_owner.py | 37 + .../models/api_key_validation_response.py | 40 + .../api_keys/models/validate_api_key.py | 33 + .../application_client_secrets/__init__.py | 4 + .../application_client_secrets/_resource.py | 209 + .../models/__init__.py | 11 + .../application_credentials_list_item.py | 65 + .../models/create_application_secret.py | 29 + .../models/new_connect_application_secret.py | 69 + src/workos/applications/__init__.py | 4 + src/workos/applications/_resource.py | 418 ++ src/workos/applications/models/__init__.py | 8 + .../applications/models/applications_order.py | 29 + .../models/connect_application.py | 73 + .../models/create_m2m_application.py | 55 + .../models/create_oauth_application.py | 84 + .../applications/models/redirect_uri_dto.py | 40 + .../models/update_oauth_application.py | 63 + src/workos/async_client.py | 7 + src/workos/audit_logs/__init__.py | 4 + src/workos/audit_logs/_resource.py | 609 +++ src/workos/audit_logs/models/__init__.py | 24 + .../models/audit_log_action_json.py | 62 + .../audit_logs/models/audit_log_event.py | 69 + .../models/audit_log_event_actor.py | 47 + .../models/audit_log_event_context.py | 38 + .../models/audit_log_event_create_response.py | 33 + .../models/audit_log_event_ingestion.py | 40 + .../models/audit_log_event_target.py | 47 + .../models/audit_log_export_creation.py | 66 + .../models/audit_log_export_json.py | 66 + .../audit_logs/models/audit_log_schema.py | 52 + .../models/audit_log_schema_actor.py | 33 + .../models/audit_log_schema_json.py | 69 + .../models/audit_log_schema_json_actor.py | 5 + .../models/audit_log_schema_json_target.py | 38 + .../models/audit_log_schema_target.py | 5 + .../audit_logs/models/audit_logs_order.py | 6 + src/workos/authorization/__init__.py | 4 + src/workos/authorization/_resource.py | 2747 ++++++++++ src/workos/authorization/models/__init__.py | 32 + .../models/add_role_permission.py | 33 + .../authorization/models/assign_role.py | 48 + src/workos/authorization/models/assignment.py | 6 + .../models/authorization_assignment.py | 28 + .../models/authorization_check.py | 33 + .../models/authorization_order.py | 6 + .../models/authorization_resource.py | 84 + .../models/check_authorization.py | 48 + .../models/create_authorization_resource.py | 69 + .../models/create_organization_role.py | 44 + .../authorization/models/create_role.py | 49 + src/workos/authorization/models/list.py | 42 + src/workos/authorization/models/list_data.py | 82 + .../authorization/models/remove_role.py | 48 + src/workos/authorization/models/role.py | 82 + .../authorization/models/role_assignment.py | 67 + .../models/role_assignment_resource.py | 41 + src/workos/authorization/models/role_list.py | 42 + .../models/set_role_permissions.py | 33 + src/workos/authorization/models/slim_role.py | 33 + .../models/update_authorization_resource.py | 56 + .../models/update_organization_role.py | 41 + .../authorization/models/update_role.py | 41 + ..._organization_membership_base_list_data.py | 83 + src/workos/client.py | 8 + src/workos/common/__init__.py | 81 + src/workos/common/models/__init__.py | 123 + ...udit_log_configuration_log_stream_state.py | 34 + ...audit_log_configuration_log_stream_type.py | 34 + .../models/audit_log_configuration_state.py | 29 + .../models/audit_log_export_json_state.py | 29 + .../common/models/audit_log_export_state.py | 6 + .../common/models/audit_log_stream_state.py | 8 + .../common/models/audit_log_trail_state.py | 6 + src/workos/common/models/auth_method_type.py | 6 + ...enticate_response_authentication_method.py | 73 + .../authentication_factor_enrolled_type.py | 32 + .../models/authentication_factor_type.py | 6 + ...hentication_factors_create_request_type.py | 33 + .../common/models/connected_account_state.py | 31 + src/workos/common/models/connection_state.py | 34 + src/workos/common/models/connection_status.py | 28 + src/workos/common/models/connection_type.py | 129 + .../create_user_dto_password_hash_type.py | 34 + .../create_user_invite_options_dto_locale.py | 207 + .../create_webhook_endpoint_dto_events.py | 169 + ...t_response_data_connected_account_state.py | 6 + ...tegrations_list_response_data_ownership.py | 32 + src/workos/common/models/directory_state.py | 39 + src/workos/common/models/directory_type.py | 75 + .../directory_user_with_groups_state.py | 31 + .../common/models/generate_link_dto_intent.py | 41 + src/workos/common/models/invitation_state.py | 30 + src/workos/common/models/list_data_type.py | 28 + .../organization_domain_data_dto_state.py | 28 + .../organization_domain_stand_alone_state.py | 33 + ...omain_stand_alone_verification_strategy.py | 32 + .../models/organization_domain_state.py | 6 + ...ganization_domain_verification_strategy.py | 10 + .../models/organization_membership_status.py | 31 + .../common/models/password_hash_type.py | 6 + .../common/models/profile_connection_type.py | 6 + .../radar_standalone_assess_request_action.py | 36 + ...r_standalone_assess_request_auth_method.py | 45 + ...adar_standalone_response_blocklist_type.py | 43 + .../radar_standalone_response_control.py | 47 + .../radar_standalone_response_verdict.py | 31 + .../resend_user_invite_options_dto_locale.py | 6 + src/workos/common/models/role_type.py | 6 + .../update_user_dto_password_hash_type.py | 6 + .../update_webhook_endpoint_dto_events.py | 6 + .../update_webhook_endpoint_dto_status.py | 28 + .../user_identities_get_item_provider.py | 55 + src/workos/common/models/user_invite_state.py | 6 + ...zation_membership_base_list_data_status.py | 6 + .../user_organization_membership_status.py | 6 + .../models/user_sessions_auth_method.py | 47 + .../common/models/user_sessions_status.py | 29 + .../models/webhook_endpoint_json_status.py | 6 + .../models/widget_session_token_dto_scopes.py | 39 + src/workos/connections/__init__.py | 4 + src/workos/connections/_resource.py | 267 + src/workos/connections/models/__init__.py | 9 + src/workos/connections/models/connection.py | 99 + .../connections/models/connection_domain.py | 41 + .../connections/models/connection_option.py | 36 + .../models/connections_connection_type.py | 123 + .../connections/models/connections_order.py | 6 + src/workos/directories/__init__.py | 4 + src/workos/directories/_resource.py | 263 + src/workos/directories/models/__init__.py | 6 + .../directories/models/directories_order.py | 6 + src/workos/directories/models/directory.py | 91 + .../directories/models/directory_metadata.py | 42 + .../models/directory_metadata_user.py | 37 + src/workos/directory_groups/__init__.py | 4 + src/workos/directory_groups/_resource.py | 203 + .../directory_groups/models/__init__.py | 4 + .../models/directory_group.py | 75 + .../models/directory_groups_order.py | 6 + src/workos/directory_users/__init__.py | 4 + src/workos/directory_users/_resource.py | 203 + src/workos/directory_users/models/__init__.py | 9 + .../models/directory_user_with_groups.py | 153 + .../directory_user_with_groups_email.py | 46 + .../models/directory_users_order.py | 6 + src/workos/events/__init__.py | 4 + src/workos/events/_resource.py | 151 + src/workos/events/models/__init__.py | 4 + src/workos/events/models/event.py | 59 + src/workos/events/models/events_order.py | 6 + src/workos/exceptions.py | 3 + src/workos/feature_flags/__init__.py | 4 + src/workos/feature_flags/_resource.py | 309 ++ src/workos/feature_flags/models/__init__.py | 7 + .../feature_flags/models/feature_flag.py | 93 + .../models/feature_flag_owner.py | 47 + .../models/feature_flags_order.py | 6 + src/workos/feature_flags/models/flag.py | 93 + src/workos/feature_flags/models/flag_owner.py | 5 + src/workos/feature_flags/targets/__init__.py | 4 + src/workos/feature_flags/targets/_resource.py | 144 + .../feature_flags/targets/models/__init__.py | 2 + src/workos/multi_factor_auth/__init__.py | 4 + src/workos/multi_factor_auth/_resource.py | 321 ++ .../multi_factor_auth/challenges/__init__.py | 4 + .../multi_factor_auth/challenges/_resource.py | 101 + .../challenges/models/__init__.py | 9 + .../models/authentication_challenge.py | 72 + ...uthentication_challenge_verify_response.py | 41 + ...uthentication_challenges_verify_request.py | 33 + .../multi_factor_auth/models/__init__.py | 24 + .../models/authentication_factor.py | 82 + .../models/authentication_factor_enrolled.py | 84 + .../authentication_factor_enrolled_sms.py | 33 + .../authentication_factor_enrolled_totp.py | 49 + .../models/authentication_factor_sms.py | 5 + .../models/authentication_factor_totp.py | 37 + .../authentication_factors_create_request.py | 54 + .../models/challenge_authentication_factor.py | 34 + src/workos/organization_domains/__init__.py | 4 + src/workos/organization_domains/_resource.py | 287 ++ .../organization_domains/models/__init__.py | 9 + .../models/create_organization_domain.py | 37 + .../models/organization_domain.py | 88 + .../models/organization_domain_stand_alone.py | 92 + src/workos/organizations/__init__.py | 4 + src/workos/organizations/_resource.py | 750 +++ src/workos/organizations/api_keys/__init__.py | 4 + .../organizations/api_keys/_resource.py | 214 + .../organizations/api_keys/models/__init__.py | 10 + .../api_keys/models/api_key_with_value.py | 86 + .../models/api_key_with_value_owner.py | 5 + .../models/create_organization_api_key.py | 38 + .../models/organizations_api_keys_order.py | 6 + .../organizations/feature_flags/__init__.py | 4 + .../organizations/feature_flags/_resource.py | 125 + .../feature_flags/models/__init__.py | 5 + .../organizations_feature_flags_order.py | 6 + src/workos/organizations/models/__init__.py | 15 + .../models/audit_log_configuration.py | 54 + .../audit_log_configuration_log_stream.py | 59 + .../models/audit_logs_retention_json.py | 36 + .../organizations/models/organization.py | 92 + .../models/organization_domain_data.py | 38 + .../organizations/models/organization_dto.py | 74 + .../models/organizations_order.py | 6 + .../models/update_audit_logs_retention.py | 33 + .../models/update_organization.py | 80 + src/workos/permissions/__init__.py | 4 + src/workos/permissions/_resource.py | 429 ++ src/workos/permissions/models/__init__.py | 11 + .../models/authorization_permission.py | 77 + .../models/create_authorization_permission.py | 49 + src/workos/permissions/models/permission.py | 5 + .../permissions/models/permissions_order.py | 6 + .../models/update_authorization_permission.py | 41 + src/workos/pipes/__init__.py | 4 + src/workos/pipes/_resource.py | 212 + src/workos/pipes/models/__init__.py | 14 + .../data_integration_access_token_response.py | 29 + ...data_integration_authorize_url_response.py | 33 + ..._data_integration_authorize_url_request.py | 45 + ...ata_integrations_get_user_token_request.py | 38 + src/workos/radar/__init__.py | 4 + src/workos/radar/_resource.py | 368 ++ src/workos/radar/models/__init__.py | 22 + src/workos/radar/models/radar_action.py | 28 + ...dar_list_entry_already_present_response.py | 33 + .../models/radar_standalone_assess_request.py | 61 + ...ndalone_delete_radar_list_entry_request.py | 35 + .../radar/models/radar_standalone_response.py | 58 + ...standalone_update_radar_attempt_request.py | 41 + ...ar_standalone_update_radar_list_request.py | 33 + src/workos/radar/models/radar_type.py | 6 + src/workos/sso/__init__.py | 4 + src/workos/sso/_resource.py | 642 +++ src/workos/sso/models/__init__.py | 18 + src/workos/sso/models/profile.py | 113 + .../sso/models/sso_authorize_url_response.py | 33 + .../models/sso_logout_authorize_request.py | 33 + .../models/sso_logout_authorize_response.py | 37 + src/workos/sso/models/sso_provider.py | 32 + src/workos/sso/models/sso_token_response.py | 57 + .../models/sso_token_response_oauth_token.py | 49 + src/workos/sso/models/token_query.py | 45 + src/workos/types/__init__.py | 2 + src/workos/types/admin_portal/__init__.py | 3 + src/workos/types/api_keys/__init__.py | 3 + .../application_client_secrets/__init__.py | 3 + src/workos/types/applications/__init__.py | 3 + src/workos/types/audit_logs/__init__.py | 3 + src/workos/types/authorization/__init__.py | 3 + src/workos/types/connect/__init__.py | 3 + src/workos/types/connections/__init__.py | 3 + src/workos/types/directories/__init__.py | 3 + src/workos/types/directory_groups/__init__.py | 3 + src/workos/types/directory_sync/__init__.py | 3 + src/workos/types/directory_users/__init__.py | 3 + src/workos/types/events/__init__.py | 3 + src/workos/types/feature_flags/__init__.py | 4 + .../types/feature_flags/feature_flag.py | 3 + .../types/feature_flags/feature_flag_owner.py | 3 + src/workos/types/feature_flags/flag.py | 3 + src/workos/types/feature_flags/flag_owner.py | 3 + .../types/feature_flags_targets/__init__.py | 3 + src/workos/types/fga/__init__.py | 3 + src/workos/types/mfa/__init__.py | 3 + .../types/multi_factor_auth/__init__.py | 4 + .../authentication_challenge.py | 5 + ...uthentication_challenge_verify_response.py | 5 + ...uthentication_challenges_verify_request.py | 5 + .../authentication_factor.py | 3 + .../authentication_factor_enrolled.py | 5 + .../authentication_factor_enrolled_sms.py | 5 + .../authentication_factor_enrolled_totp.py | 5 + .../authentication_factor_sms.py | 5 + .../authentication_factor_totp.py | 5 + .../authentication_factors_create_request.py | 5 + .../challenge_authentication_factor.py | 5 + .../multi_factor_auth_challenges/__init__.py | 3 + .../types/organization_domains/__init__.py | 3 + src/workos/types/organizations/__init__.py | 5 + .../types/organizations/api_key_with_value.py | 3 + .../organizations/api_key_with_value_owner.py | 5 + .../organizations/audit_log_configuration.py | 3 + .../audit_log_configuration_log_stream.py | 5 + .../audit_logs_retention_json.py | 3 + .../create_organization_api_key.py | 5 + .../types/organizations/organization.py | 3 + .../organizations/organization_domain_data.py | 3 + .../types/organizations/organization_dto.py | 3 + .../update_audit_logs_retention.py | 5 + .../organizations/update_organization.py | 3 + .../types/organizations_api_keys/__init__.py | 3 + .../organizations_feature_flags/__init__.py | 3 + src/workos/types/permissions/__init__.py | 3 + src/workos/types/pipes/__init__.py | 3 + src/workos/types/portal/__init__.py | 3 + src/workos/types/radar/__init__.py | 3 + src/workos/types/sso/__init__.py | 3 + src/workos/types/user_management/__init__.py | 13 + .../user_management/authenticate_response.py | 5 + .../authenticate_response_impersonator.py | 5 + .../authenticate_response_oauth_token.py | 5 + ...ation_code_session_authenticate_request.py | 5 + .../user_management/connected_account.py | 5 + .../user_management/cors_origin_response.py | 5 + .../user_management/create_cors_origin.py | 5 + .../create_magic_code_and_return.py | 5 + .../user_management/create_password_reset.py | 5 + .../create_password_reset_token.py | 5 + .../user_management/create_redirect_uri.py | 5 + .../types/user_management/create_user.py | 3 + .../create_user_invite_options.py | 5 + .../create_user_organization_membership.py | 5 + .../data_integrations_list_response.py | 5 + .../data_integrations_list_response_data.py | 5 + ...ns_list_response_data_connected_account.py | 5 + .../device_authorization_response.py | 5 + .../user_management/email_verification.py | 3 + .../enroll_user_authentication_factor.py | 5 + .../types/user_management/invitation.py | 3 + .../types/user_management/jwks_response.py | 3 + .../user_management/jwks_response_keys.py | 5 + .../user_management/jwt_template_response.py | 5 + .../types/user_management/magic_auth.py | 3 + .../organization_membership.py | 5 + .../types/user_management/password_reset.py | 3 + .../password_session_authenticate_request.py | 5 + .../types/user_management/redirect_uri.py | 3 + ...resh_token_session_authenticate_request.py | 5 + .../resend_user_invite_options.py | 5 + .../reset_password_response.py | 5 + .../types/user_management/revoke_session.py | 3 + .../send_verification_email_response.py | 5 + .../sso_device_authorization_request.py | 5 + .../user_management/update_jwt_template.py | 5 + .../types/user_management/update_user.py | 3 + .../update_user_organization_membership.py | 5 + ...evice_code_session_authenticate_request.py | 5 + ...ation_code_session_authenticate_request.py | 5 + ..._auth_code_session_authenticate_request.py | 5 + ...e_mfa_totp_session_authenticate_request.py | 5 + ..._selection_session_authenticate_request.py | 5 + src/workos/types/user_management/user.py | 3 + ...r_authentication_factor_enroll_response.py | 5 + .../user_identities_get_item.py | 5 + .../types/user_management/user_invite.py | 3 + .../user_organization_membership.py | 5 + .../user_sessions_impersonator.py | 5 + .../user_sessions_list_item.py | 5 + .../user_management/verify_email_address.py | 3 + .../user_management/verify_email_response.py | 5 + .../__init__.py | 3 + .../user_management_cors_origins/__init__.py | 3 + .../__init__.py | 3 + .../user_management_invitations/__init__.py | 3 + .../user_management_jwt_template/__init__.py | 3 + .../user_management_magic_auth/__init__.py | 3 + .../__init__.py | 3 + .../__init__.py | 3 + .../user_management_redirect_uris/__init__.py | 3 + .../__init__.py | 3 + .../types/user_management_users/__init__.py | 4 + ...uthorized_connect_application_list_data.py | 5 + .../__init__.py | 3 + .../__init__.py | 3 + src/workos/types/webhooks/__init__.py | 3 + src/workos/types/widgets/__init__.py | 3 + src/workos/types/workos_connect/__init__.py | 3 + src/workos/user_management/__init__.py | 2 + .../authentication/__init__.py | 4 + .../authentication/_resource.py | 496 ++ .../authentication/models/__init__.py | 48 + .../models/authenticate_response.py | 82 + .../authenticate_response_impersonator.py | 40 + .../authenticate_response_oauth_token.py | 49 + ...ation_code_session_authenticate_request.py | 61 + .../models/device_authorization_response.py | 55 + .../password_session_authenticate_request.py | 68 + ...resh_token_session_authenticate_request.py | 66 + .../authentication/models/revoke_session.py | 38 + .../authentication/models/screen_hint_type.py | 8 + .../sso_device_authorization_request.py | 33 + ...evice_code_session_authenticate_request.py | 57 + ...ation_code_session_authenticate_request.py | 65 + ..._auth_code_session_authenticate_request.py | 70 + ...e_mfa_totp_session_authenticate_request.py | 69 + ..._selection_session_authenticate_request.py | 65 + .../authentication/models/user.py | 113 + ...user_management_authentication_provider.py | 35 + ...r_management_authentication_screen_hint.py | 30 + .../user_management/cors_origins/__init__.py | 4 + .../user_management/cors_origins/_resource.py | 95 + .../cors_origins/models/__init__.py | 4 + .../models/cors_origin_response.py | 58 + .../cors_origins/models/create_cors_origin.py | 33 + .../data_providers/__init__.py | 4 + .../data_providers/_resource.py | 269 + .../data_providers/models/__init__.py | 12 + .../models/connected_account.py | 71 + .../models/data_integrations_list_response.py | 45 + .../data_integrations_list_response_data.py | 96 + ...ns_list_response_data_connected_account.py | 82 + .../user_management/invitations/__init__.py | 4 + .../user_management/invitations/_resource.py | 573 +++ .../invitations/models/__init__.py | 13 + .../models/create_user_invite_options.py | 61 + .../invitations/models/invitation.py | 122 + .../models/resend_user_invite_options.py | 37 + .../invitations/models/user_invite.py | 122 + .../user_management_invitations_order.py | 6 + .../user_management/jwt_template/__init__.py | 4 + .../user_management/jwt_template/_resource.py | 93 + .../jwt_template/models/__init__.py | 4 + .../models/jwt_template_response.py | 45 + .../models/update_jwt_template.py | 33 + .../user_management/magic_auth/__init__.py | 4 + .../user_management/magic_auth/_resource.py | 169 + .../magic_auth/models/__init__.py | 6 + .../models/create_magic_code_and_return.py | 38 + .../magic_auth/models/magic_auth.py | 74 + .../multi_factor_authentication/__init__.py | 7 + .../multi_factor_authentication/_resource.py | 224 + .../models/__init__.py | 11 + .../enroll_user_authentication_factor.py | 48 + ...r_authentication_factor_enroll_response.py | 46 + ...ement_multi_factor_authentication_order.py | 6 + .../organization_membership/__init__.py | 7 + .../organization_membership/_resource.py | 596 +++ .../models/__init__.py | 18 + .../create_user_organization_membership.py | 47 + .../models/organization_membership.py | 88 + .../update_user_organization_membership.py | 39 + ...anagement_organization_membership_order.py | 6 + ...gement_organization_membership_statuses.py | 6 + .../models/user_organization_membership.py | 88 + .../user_management/redirect_uris/__init__.py | 4 + .../redirect_uris/_resource.py | 95 + .../redirect_uris/models/__init__.py | 4 + .../models/create_redirect_uri.py | 33 + .../redirect_uris/models/redirect_uri.py | 53 + .../session_tokens/__init__.py | 4 + .../session_tokens/_resource.py | 85 + .../session_tokens/models/__init__.py | 4 + .../session_tokens/models/jwks_response.py | 39 + .../models/jwks_response_keys.py | 61 + src/workos/user_management/users/__init__.py | 4 + src/workos/user_management/users/_resource.py | 1172 +++++ .../user_management/users/models/__init__.py | 24 + .../users/models/create_password_reset.py | 37 + .../models/create_password_reset_token.py | 33 + .../users/models/create_user.py | 88 + .../users/models/email_verification.py | 74 + .../users/models/password_reset.py | 70 + .../users/models/reset_password_response.py | 36 + .../send_verification_email_response.py | 36 + .../users/models/update_user.py | 88 + .../users/models/user_identities_get_item.py | 42 + .../models/user_management_users_order.py | 6 + .../models/user_sessions_impersonator.py | 40 + .../users/models/user_sessions_list_item.py | 118 + .../users/models/verify_email_address.py | 33 + .../users/models/verify_email_response.py | 36 + src/workos/user_management_users/__init__.py | 2 + .../authorized_applications/__init__.py | 7 + .../authorized_applications/_resource.py | 183 + .../models/__init__.py | 8 + ...uthorized_connect_application_list_data.py | 49 + ...ent_users_authorized_applications_order.py | 6 + .../feature_flags/__init__.py | 7 + .../feature_flags/_resource.py | 125 + .../feature_flags/models/__init__.py | 5 + ...er_management_users_feature_flags_order.py | 6 + src/workos/webhooks/__init__.py | 4 + src/workos/webhooks/_resource.py | 441 ++ src/workos/webhooks/models/__init__.py | 6 + .../models/create_webhook_endpoint.py | 42 + .../models/update_webhook_endpoint.py | 53 + .../webhooks/models/webhook_endpoint_json.py | 71 + src/workos/webhooks/models/webhooks_order.py | 6 + src/workos/widgets/__init__.py | 4 + src/workos/widgets/_resource.py | 118 + src/workos/widgets/models/__init__.py | 6 + .../widgets/models/widget_session_token.py | 49 + .../models/widget_session_token_response.py | 33 + src/workos/workos_connect/__init__.py | 4 + src/workos/workos_connect/_resource.py | 143 + src/workos/workos_connect/models/__init__.py | 13 + .../models/external_auth_complete_response.py | 33 + .../models/user_consent_option.py | 51 + .../models/user_consent_option_choice.py | 39 + .../models/user_management_login_request.py | 53 + .../workos_connect/models/user_object.py | 52 + tests/conftest.py | 26 +- tests/fixtures/add_role_permission.json | 3 + tests/fixtures/api_key.json | 17 + tests/fixtures/api_key_owner.json | 4 + .../fixtures/api_key_validation_response.json | 19 + tests/fixtures/api_key_with_value.json | 18 + tests/fixtures/api_key_with_value_owner.json | 4 + .../application_credentials_list_item.json | 8 + tests/fixtures/assign_role.json | 6 + tests/fixtures/audit_log_action_json.json | 42 + tests/fixtures/audit_log_configuration.json | 12 + .../audit_log_configuration_log_stream.json | 7 + tests/fixtures/audit_log_event.json | 30 + tests/fixtures/audit_log_event_actor.json | 8 + tests/fixtures/audit_log_event_context.json | 4 + .../audit_log_event_create_response.json | 3 + tests/fixtures/audit_log_event_ingestion.json | 33 + tests/fixtures/audit_log_event_target.json | 8 + tests/fixtures/audit_log_export_creation.json | 20 + tests/fixtures/audit_log_export_json.json | 8 + tests/fixtures/audit_log_schema.json | 33 + tests/fixtures/audit_log_schema_actor.json | 10 + tests/fixtures/audit_log_schema_json.json | 36 + .../fixtures/audit_log_schema_json_actor.json | 5 + .../audit_log_schema_json_target.json | 11 + tests/fixtures/audit_log_schema_target.json | 11 + tests/fixtures/audit_logs_retention_json.json | 3 + tests/fixtures/authenticate_response.json | 39 + .../authenticate_response_impersonator.json | 4 + .../authenticate_response_oauth_token.json | 11 + tests/fixtures/authentication_challenge.json | 9 + ...hentication_challenge_verify_response.json | 12 + ...hentication_challenges_verify_request.json | 3 + tests/fixtures/authentication_factor.json | 15 + .../authentication_factor_enrolled.json | 18 + .../authentication_factor_enrolled_sms.json | 3 + .../authentication_factor_enrolled_totp.json | 7 + tests/fixtures/authentication_factor_sms.json | 3 + .../fixtures/authentication_factor_totp.json | 4 + ...authentication_factors_create_request.json | 7 + tests/fixtures/authorization_check.json | 3 + ...ion_code_session_authenticate_request.json | 9 + tests/fixtures/authorization_permission.json | 11 + tests/fixtures/authorization_resource.json | 12 + ...horized_connect_application_list_data.json | 23 + .../challenge_authentication_factor.json | 3 + tests/fixtures/check_authorization.json | 6 + tests/fixtures/connect_application.json | 14 + tests/fixtures/connected_account.json | 13 + tests/fixtures/connection.json | 21 + tests/fixtures/connection_domain.json | 5 + tests/fixtures/connection_option.json | 3 + tests/fixtures/cors_origin_response.json | 7 + tests/fixtures/create_application_secret.json | 1 + .../create_authorization_permission.json | 6 + .../create_authorization_resource.json | 10 + tests/fixtures/create_cors_origin.json | 3 + tests/fixtures/create_m2m_application.json | 11 + .../create_magic_code_and_return.json | 4 + tests/fixtures/create_oauth_application.json | 19 + .../fixtures/create_organization_api_key.json | 7 + .../fixtures/create_organization_domain.json | 4 + tests/fixtures/create_organization_role.json | 5 + tests/fixtures/create_password_reset.json | 4 + .../fixtures/create_password_reset_token.json | 3 + tests/fixtures/create_redirect_uri.json | 3 + tests/fixtures/create_role.json | 6 + tests/fixtures/create_user.json | 13 + .../fixtures/create_user_invite_options.json | 8 + .../create_user_organization_membership.json | 8 + tests/fixtures/create_webhook_endpoint.json | 7 + ...ata_integration_access_token_response.json | 1 + ...ta_integration_authorize_url_response.json | 3 + ...ata_integration_authorize_url_request.json | 5 + ...a_integrations_get_user_token_request.json | 4 + .../data_integrations_list_response.json | 35 + .../data_integrations_list_response_data.json | 30 + ..._list_response_data_connected_account.json | 14 + .../device_authorization_response.json | 8 + tests/fixtures/directory.json | 19 + tests/fixtures/directory_group.json | 13 + tests/fixtures/directory_metadata.json | 7 + tests/fixtures/directory_metadata_user.json | 4 + .../fixtures/directory_user_with_groups.json | 52 + .../directory_user_with_groups_email.json | 5 + tests/fixtures/email_verification.json | 10 + .../enroll_user_authentication_factor.json | 6 + tests/fixtures/event.json | 29 + .../external_auth_complete_response.json | 3 + tests/fixtures/feature_flag.json | 19 + tests/fixtures/feature_flag_owner.json | 5 + tests/fixtures/flag.json | 19 + tests/fixtures/flag_owner.json | 5 + tests/fixtures/generate_link.json | 12 + tests/fixtures/intent_options.json | 6 + tests/fixtures/invitation.json | 16 + tests/fixtures/jwks_response.json | 16 + tests/fixtures/jwks_response_keys.json | 12 + tests/fixtures/jwt_template_response.json | 6 + tests/fixtures/list.json | 20 + tests/fixtures/list_api_key.json | 25 + .../fixtures/list_audit_log_action_json.json | 50 + .../fixtures/list_audit_log_schema_json.json | 44 + .../fixtures/list_authentication_factor.json | 23 + .../list_authorization_permission.json | 19 + .../fixtures/list_authorization_resource.json | 20 + ...horized_connect_application_list_data.json | 31 + tests/fixtures/list_connect_application.json | 22 + tests/fixtures/list_connection.json | 29 + tests/fixtures/list_data.json | 15 + tests/fixtures/list_directory.json | 27 + tests/fixtures/list_directory_group.json | 21 + .../list_directory_user_with_groups.json | 60 + tests/fixtures/list_event.json | 37 + tests/fixtures/list_flag.json | 27 + tests/fixtures/list_organization.json | 35 + tests/fixtures/list_role_assignment.json | 22 + tests/fixtures/list_user.json | 25 + tests/fixtures/list_user_invite.json | 24 + .../list_user_organization_membership.json | 27 + ...rganization_membership_base_list_data.json | 24 + .../list_user_sessions_list_item.json | 26 + .../fixtures/list_webhook_endpoint_json.json | 21 + tests/fixtures/magic_auth.json | 10 + .../new_connect_application_secret.json | 9 + tests/fixtures/organization.json | 27 + tests/fixtures/organization_domain.json | 12 + tests/fixtures/organization_domain_data.json | 4 + .../organization_domain_stand_alone.json | 12 + tests/fixtures/organization_dto.json | 17 + tests/fixtures/organization_membership.json | 19 + tests/fixtures/password_reset.json | 10 + ...password_session_authenticate_request.json | 11 + tests/fixtures/permission.json | 11 + tests/fixtures/portal_link_response.json | 3 + tests/fixtures/profile.json | 29 + ...r_list_entry_already_present_response.json | 3 + .../radar_standalone_assess_request.json | 9 + ...alone_delete_radar_list_entry_request.json | 3 + tests/fixtures/radar_standalone_response.json | 7 + ...andalone_update_radar_attempt_request.json | 4 + ..._standalone_update_radar_list_request.json | 3 + tests/fixtures/redirect_uri.json | 8 + tests/fixtures/redirect_uri_dto.json | 4 + ...sh_token_session_authenticate_request.json | 10 + tests/fixtures/remove_role.json | 6 + .../fixtures/resend_user_invite_options.json | 3 + tests/fixtures/reset_password_response.json | 19 + tests/fixtures/revoke_session.json | 4 + tests/fixtures/role.json | 15 + tests/fixtures/role_assignment.json | 14 + tests/fixtures/role_assignment_resource.json | 5 + tests/fixtures/role_list.json | 20 + .../send_verification_email_response.json | 19 + tests/fixtures/set_role_permissions.json | 8 + tests/fixtures/slim_role.json | 3 + .../fixtures/sso_authorize_url_response.json | 3 + .../sso_device_authorization_request.json | 3 + tests/fixtures/sso_intent_options.json | 4 + .../sso_logout_authorize_request.json | 3 + .../sso_logout_authorize_response.json | 4 + tests/fixtures/sso_token_response.json | 45 + .../sso_token_response_oauth_token.json | 11 + tests/fixtures/token_query.json | 6 + .../fixtures/update_audit_logs_retention.json | 3 + .../update_authorization_permission.json | 4 + .../update_authorization_resource.json | 7 + tests/fixtures/update_jwt_template.json | 3 + tests/fixtures/update_oauth_application.json | 15 + tests/fixtures/update_organization.json | 18 + tests/fixtures/update_organization_role.json | 4 + tests/fixtures/update_role.json | 4 + tests/fixtures/update_user.json | 14 + .../update_user_organization_membership.json | 6 + tests/fixtures/update_webhook_endpoint.json | 8 + ...ice_code_session_authenticate_request.json | 8 + ...ion_code_session_authenticate_request.json | 10 + ...uth_code_session_authenticate_request.json | 11 + ...mfa_totp_session_authenticate_request.json | 11 + ...election_session_authenticate_request.json | 10 + tests/fixtures/user.json | 17 + ...authentication_factor_enroll_response.json | 29 + tests/fixtures/user_consent_option.json | 11 + .../fixtures/user_consent_option_choice.json | 4 + tests/fixtures/user_identities_get_item.json | 5 + tests/fixtures/user_invite.json | 16 + .../user_management_login_request.json | 26 + tests/fixtures/user_object.json | 10 + .../user_organization_membership.json | 19 + ...rganization_membership_base_list_data.json | 16 + .../fixtures/user_sessions_impersonator.json | 4 + tests/fixtures/user_sessions_list_item.json | 18 + tests/fixtures/validate_api_key.json | 3 + tests/fixtures/verify_email_address.json | 3 + tests/fixtures/verify_email_response.json | 19 + tests/fixtures/webhook_endpoint_json.json | 13 + tests/fixtures/widget_session_token.json | 7 + .../widget_session_token_response.json | 3 + tests/generated_helpers.py | 14 + tests/test_admin_portal.py | 131 + tests/test_api_keys.py | 137 + tests/test_application_client_secrets.py | 161 + tests/test_applications.py | 245 + tests/test_audit_logs.py | 331 ++ tests/test_authorization.py | 1298 +++++ tests/test_connections.py | 213 + tests/test_directories.py | 205 + tests/test_directory_groups.py | 185 + tests/test_directory_users.py | 187 + tests/test_events.py | 167 + tests/test_feature_flags.py | 221 + tests/test_feature_flags_targets.py | 156 + tests/test_generated_client.py | 297 ++ tests/test_models_round_trip.py | 4479 +++++++++++++++++ tests/test_multi_factor_auth.py | 212 + tests/test_multi_factor_auth_challenges.py | 137 + tests/test_organization_domains.py | 221 + tests/test_organizations.py | 343 ++ tests/test_organizations_api_keys.py | 189 + tests/test_organizations_feature_flags.py | 161 + tests/test_pagination.py | 108 + tests/test_permissions.py | 247 + tests/test_pipes.py | 182 + tests/test_radar.py | 258 + tests/test_sso.py | 224 + tests/test_user_management_authentication.py | 246 + tests/test_user_management_cors_origins.py | 143 + tests/test_user_management_data_providers.py | 269 + tests/test_user_management_invitations.py | 310 ++ tests/test_user_management_jwt_template.py | 145 + tests/test_user_management_magic_auth.py | 171 + ..._management_multi_factor_authentication.py | 215 + ...user_management_organization_membership.py | 333 ++ tests/test_user_management_redirect_uris.py | 125 + tests/test_user_management_session_tokens.py | 128 + tests/test_user_management_users.py | 512 ++ ...anagement_users_authorized_applications.py | 205 + ...est_user_management_users_feature_flags.py | 163 + tests/test_webhooks.py | 221 + tests/test_widgets.py | 145 + tests/test_workos_connect.py | 162 + 753 files changed, 44973 insertions(+), 66 deletions(-) create mode 100644 src/workos/_errors.py create mode 100644 src/workos/_pagination.py create mode 100644 src/workos/_types.py create mode 100644 src/workos/admin_portal/__init__.py create mode 100644 src/workos/admin_portal/_resource.py create mode 100644 src/workos/admin_portal/models/__init__.py create mode 100644 src/workos/admin_portal/models/generate_link.py create mode 100644 src/workos/admin_portal/models/intent_options.py create mode 100644 src/workos/admin_portal/models/portal_link_response.py create mode 100644 src/workos/admin_portal/models/sso_intent_options.py create mode 100644 src/workos/api_keys/__init__.py create mode 100644 src/workos/api_keys/_resource.py create mode 100644 src/workos/api_keys/models/__init__.py create mode 100644 src/workos/api_keys/models/api_key.py create mode 100644 src/workos/api_keys/models/api_key_owner.py create mode 100644 src/workos/api_keys/models/api_key_validation_response.py create mode 100644 src/workos/api_keys/models/validate_api_key.py create mode 100644 src/workos/application_client_secrets/__init__.py create mode 100644 src/workos/application_client_secrets/_resource.py create mode 100644 src/workos/application_client_secrets/models/__init__.py create mode 100644 src/workos/application_client_secrets/models/application_credentials_list_item.py create mode 100644 src/workos/application_client_secrets/models/create_application_secret.py create mode 100644 src/workos/application_client_secrets/models/new_connect_application_secret.py create mode 100644 src/workos/applications/__init__.py create mode 100644 src/workos/applications/_resource.py create mode 100644 src/workos/applications/models/__init__.py create mode 100644 src/workos/applications/models/applications_order.py create mode 100644 src/workos/applications/models/connect_application.py create mode 100644 src/workos/applications/models/create_m2m_application.py create mode 100644 src/workos/applications/models/create_oauth_application.py create mode 100644 src/workos/applications/models/redirect_uri_dto.py create mode 100644 src/workos/applications/models/update_oauth_application.py create mode 100644 src/workos/async_client.py create mode 100644 src/workos/audit_logs/__init__.py create mode 100644 src/workos/audit_logs/_resource.py create mode 100644 src/workos/audit_logs/models/__init__.py create mode 100644 src/workos/audit_logs/models/audit_log_action_json.py create mode 100644 src/workos/audit_logs/models/audit_log_event.py create mode 100644 src/workos/audit_logs/models/audit_log_event_actor.py create mode 100644 src/workos/audit_logs/models/audit_log_event_context.py create mode 100644 src/workos/audit_logs/models/audit_log_event_create_response.py create mode 100644 src/workos/audit_logs/models/audit_log_event_ingestion.py create mode 100644 src/workos/audit_logs/models/audit_log_event_target.py create mode 100644 src/workos/audit_logs/models/audit_log_export_creation.py create mode 100644 src/workos/audit_logs/models/audit_log_export_json.py create mode 100644 src/workos/audit_logs/models/audit_log_schema.py create mode 100644 src/workos/audit_logs/models/audit_log_schema_actor.py create mode 100644 src/workos/audit_logs/models/audit_log_schema_json.py create mode 100644 src/workos/audit_logs/models/audit_log_schema_json_actor.py create mode 100644 src/workos/audit_logs/models/audit_log_schema_json_target.py create mode 100644 src/workos/audit_logs/models/audit_log_schema_target.py create mode 100644 src/workos/audit_logs/models/audit_logs_order.py create mode 100644 src/workos/authorization/__init__.py create mode 100644 src/workos/authorization/_resource.py create mode 100644 src/workos/authorization/models/__init__.py create mode 100644 src/workos/authorization/models/add_role_permission.py create mode 100644 src/workos/authorization/models/assign_role.py create mode 100644 src/workos/authorization/models/assignment.py create mode 100644 src/workos/authorization/models/authorization_assignment.py create mode 100644 src/workos/authorization/models/authorization_check.py create mode 100644 src/workos/authorization/models/authorization_order.py create mode 100644 src/workos/authorization/models/authorization_resource.py create mode 100644 src/workos/authorization/models/check_authorization.py create mode 100644 src/workos/authorization/models/create_authorization_resource.py create mode 100644 src/workos/authorization/models/create_organization_role.py create mode 100644 src/workos/authorization/models/create_role.py create mode 100644 src/workos/authorization/models/list.py create mode 100644 src/workos/authorization/models/list_data.py create mode 100644 src/workos/authorization/models/remove_role.py create mode 100644 src/workos/authorization/models/role.py create mode 100644 src/workos/authorization/models/role_assignment.py create mode 100644 src/workos/authorization/models/role_assignment_resource.py create mode 100644 src/workos/authorization/models/role_list.py create mode 100644 src/workos/authorization/models/set_role_permissions.py create mode 100644 src/workos/authorization/models/slim_role.py create mode 100644 src/workos/authorization/models/update_authorization_resource.py create mode 100644 src/workos/authorization/models/update_organization_role.py create mode 100644 src/workos/authorization/models/update_role.py create mode 100644 src/workos/authorization/models/user_organization_membership_base_list_data.py create mode 100644 src/workos/client.py create mode 100644 src/workos/common/__init__.py create mode 100644 src/workos/common/models/__init__.py create mode 100644 src/workos/common/models/audit_log_configuration_log_stream_state.py create mode 100644 src/workos/common/models/audit_log_configuration_log_stream_type.py create mode 100644 src/workos/common/models/audit_log_configuration_state.py create mode 100644 src/workos/common/models/audit_log_export_json_state.py create mode 100644 src/workos/common/models/audit_log_export_state.py create mode 100644 src/workos/common/models/audit_log_stream_state.py create mode 100644 src/workos/common/models/audit_log_trail_state.py create mode 100644 src/workos/common/models/auth_method_type.py create mode 100644 src/workos/common/models/authenticate_response_authentication_method.py create mode 100644 src/workos/common/models/authentication_factor_enrolled_type.py create mode 100644 src/workos/common/models/authentication_factor_type.py create mode 100644 src/workos/common/models/authentication_factors_create_request_type.py create mode 100644 src/workos/common/models/connected_account_state.py create mode 100644 src/workos/common/models/connection_state.py create mode 100644 src/workos/common/models/connection_status.py create mode 100644 src/workos/common/models/connection_type.py create mode 100644 src/workos/common/models/create_user_dto_password_hash_type.py create mode 100644 src/workos/common/models/create_user_invite_options_dto_locale.py create mode 100644 src/workos/common/models/create_webhook_endpoint_dto_events.py create mode 100644 src/workos/common/models/data_integrations_list_response_data_connected_account_state.py create mode 100644 src/workos/common/models/data_integrations_list_response_data_ownership.py create mode 100644 src/workos/common/models/directory_state.py create mode 100644 src/workos/common/models/directory_type.py create mode 100644 src/workos/common/models/directory_user_with_groups_state.py create mode 100644 src/workos/common/models/generate_link_dto_intent.py create mode 100644 src/workos/common/models/invitation_state.py create mode 100644 src/workos/common/models/list_data_type.py create mode 100644 src/workos/common/models/organization_domain_data_dto_state.py create mode 100644 src/workos/common/models/organization_domain_stand_alone_state.py create mode 100644 src/workos/common/models/organization_domain_stand_alone_verification_strategy.py create mode 100644 src/workos/common/models/organization_domain_state.py create mode 100644 src/workos/common/models/organization_domain_verification_strategy.py create mode 100644 src/workos/common/models/organization_membership_status.py create mode 100644 src/workos/common/models/password_hash_type.py create mode 100644 src/workos/common/models/profile_connection_type.py create mode 100644 src/workos/common/models/radar_standalone_assess_request_action.py create mode 100644 src/workos/common/models/radar_standalone_assess_request_auth_method.py create mode 100644 src/workos/common/models/radar_standalone_response_blocklist_type.py create mode 100644 src/workos/common/models/radar_standalone_response_control.py create mode 100644 src/workos/common/models/radar_standalone_response_verdict.py create mode 100644 src/workos/common/models/resend_user_invite_options_dto_locale.py create mode 100644 src/workos/common/models/role_type.py create mode 100644 src/workos/common/models/update_user_dto_password_hash_type.py create mode 100644 src/workos/common/models/update_webhook_endpoint_dto_events.py create mode 100644 src/workos/common/models/update_webhook_endpoint_dto_status.py create mode 100644 src/workos/common/models/user_identities_get_item_provider.py create mode 100644 src/workos/common/models/user_invite_state.py create mode 100644 src/workos/common/models/user_organization_membership_base_list_data_status.py create mode 100644 src/workos/common/models/user_organization_membership_status.py create mode 100644 src/workos/common/models/user_sessions_auth_method.py create mode 100644 src/workos/common/models/user_sessions_status.py create mode 100644 src/workos/common/models/webhook_endpoint_json_status.py create mode 100644 src/workos/common/models/widget_session_token_dto_scopes.py create mode 100644 src/workos/connections/__init__.py create mode 100644 src/workos/connections/_resource.py create mode 100644 src/workos/connections/models/__init__.py create mode 100644 src/workos/connections/models/connection.py create mode 100644 src/workos/connections/models/connection_domain.py create mode 100644 src/workos/connections/models/connection_option.py create mode 100644 src/workos/connections/models/connections_connection_type.py create mode 100644 src/workos/connections/models/connections_order.py create mode 100644 src/workos/directories/__init__.py create mode 100644 src/workos/directories/_resource.py create mode 100644 src/workos/directories/models/__init__.py create mode 100644 src/workos/directories/models/directories_order.py create mode 100644 src/workos/directories/models/directory.py create mode 100644 src/workos/directories/models/directory_metadata.py create mode 100644 src/workos/directories/models/directory_metadata_user.py create mode 100644 src/workos/directory_groups/__init__.py create mode 100644 src/workos/directory_groups/_resource.py create mode 100644 src/workos/directory_groups/models/__init__.py create mode 100644 src/workos/directory_groups/models/directory_group.py create mode 100644 src/workos/directory_groups/models/directory_groups_order.py create mode 100644 src/workos/directory_users/__init__.py create mode 100644 src/workos/directory_users/_resource.py create mode 100644 src/workos/directory_users/models/__init__.py create mode 100644 src/workos/directory_users/models/directory_user_with_groups.py create mode 100644 src/workos/directory_users/models/directory_user_with_groups_email.py create mode 100644 src/workos/directory_users/models/directory_users_order.py create mode 100644 src/workos/events/__init__.py create mode 100644 src/workos/events/_resource.py create mode 100644 src/workos/events/models/__init__.py create mode 100644 src/workos/events/models/event.py create mode 100644 src/workos/events/models/events_order.py create mode 100644 src/workos/exceptions.py create mode 100644 src/workos/feature_flags/__init__.py create mode 100644 src/workos/feature_flags/_resource.py create mode 100644 src/workos/feature_flags/models/__init__.py create mode 100644 src/workos/feature_flags/models/feature_flag.py create mode 100644 src/workos/feature_flags/models/feature_flag_owner.py create mode 100644 src/workos/feature_flags/models/feature_flags_order.py create mode 100644 src/workos/feature_flags/models/flag.py create mode 100644 src/workos/feature_flags/models/flag_owner.py create mode 100644 src/workos/feature_flags/targets/__init__.py create mode 100644 src/workos/feature_flags/targets/_resource.py create mode 100644 src/workos/feature_flags/targets/models/__init__.py create mode 100644 src/workos/multi_factor_auth/__init__.py create mode 100644 src/workos/multi_factor_auth/_resource.py create mode 100644 src/workos/multi_factor_auth/challenges/__init__.py create mode 100644 src/workos/multi_factor_auth/challenges/_resource.py create mode 100644 src/workos/multi_factor_auth/challenges/models/__init__.py create mode 100644 src/workos/multi_factor_auth/challenges/models/authentication_challenge.py create mode 100644 src/workos/multi_factor_auth/challenges/models/authentication_challenge_verify_response.py create mode 100644 src/workos/multi_factor_auth/challenges/models/authentication_challenges_verify_request.py create mode 100644 src/workos/multi_factor_auth/models/__init__.py create mode 100644 src/workos/multi_factor_auth/models/authentication_factor.py create mode 100644 src/workos/multi_factor_auth/models/authentication_factor_enrolled.py create mode 100644 src/workos/multi_factor_auth/models/authentication_factor_enrolled_sms.py create mode 100644 src/workos/multi_factor_auth/models/authentication_factor_enrolled_totp.py create mode 100644 src/workos/multi_factor_auth/models/authentication_factor_sms.py create mode 100644 src/workos/multi_factor_auth/models/authentication_factor_totp.py create mode 100644 src/workos/multi_factor_auth/models/authentication_factors_create_request.py create mode 100644 src/workos/multi_factor_auth/models/challenge_authentication_factor.py create mode 100644 src/workos/organization_domains/__init__.py create mode 100644 src/workos/organization_domains/_resource.py create mode 100644 src/workos/organization_domains/models/__init__.py create mode 100644 src/workos/organization_domains/models/create_organization_domain.py create mode 100644 src/workos/organization_domains/models/organization_domain.py create mode 100644 src/workos/organization_domains/models/organization_domain_stand_alone.py create mode 100644 src/workos/organizations/__init__.py create mode 100644 src/workos/organizations/_resource.py create mode 100644 src/workos/organizations/api_keys/__init__.py create mode 100644 src/workos/organizations/api_keys/_resource.py create mode 100644 src/workos/organizations/api_keys/models/__init__.py create mode 100644 src/workos/organizations/api_keys/models/api_key_with_value.py create mode 100644 src/workos/organizations/api_keys/models/api_key_with_value_owner.py create mode 100644 src/workos/organizations/api_keys/models/create_organization_api_key.py create mode 100644 src/workos/organizations/api_keys/models/organizations_api_keys_order.py create mode 100644 src/workos/organizations/feature_flags/__init__.py create mode 100644 src/workos/organizations/feature_flags/_resource.py create mode 100644 src/workos/organizations/feature_flags/models/__init__.py create mode 100644 src/workos/organizations/feature_flags/models/organizations_feature_flags_order.py create mode 100644 src/workos/organizations/models/__init__.py create mode 100644 src/workos/organizations/models/audit_log_configuration.py create mode 100644 src/workos/organizations/models/audit_log_configuration_log_stream.py create mode 100644 src/workos/organizations/models/audit_logs_retention_json.py create mode 100644 src/workos/organizations/models/organization.py create mode 100644 src/workos/organizations/models/organization_domain_data.py create mode 100644 src/workos/organizations/models/organization_dto.py create mode 100644 src/workos/organizations/models/organizations_order.py create mode 100644 src/workos/organizations/models/update_audit_logs_retention.py create mode 100644 src/workos/organizations/models/update_organization.py create mode 100644 src/workos/permissions/__init__.py create mode 100644 src/workos/permissions/_resource.py create mode 100644 src/workos/permissions/models/__init__.py create mode 100644 src/workos/permissions/models/authorization_permission.py create mode 100644 src/workos/permissions/models/create_authorization_permission.py create mode 100644 src/workos/permissions/models/permission.py create mode 100644 src/workos/permissions/models/permissions_order.py create mode 100644 src/workos/permissions/models/update_authorization_permission.py create mode 100644 src/workos/pipes/__init__.py create mode 100644 src/workos/pipes/_resource.py create mode 100644 src/workos/pipes/models/__init__.py create mode 100644 src/workos/pipes/models/data_integration_access_token_response.py create mode 100644 src/workos/pipes/models/data_integration_authorize_url_response.py create mode 100644 src/workos/pipes/models/data_integrations_get_data_integration_authorize_url_request.py create mode 100644 src/workos/pipes/models/data_integrations_get_user_token_request.py create mode 100644 src/workos/radar/__init__.py create mode 100644 src/workos/radar/_resource.py create mode 100644 src/workos/radar/models/__init__.py create mode 100644 src/workos/radar/models/radar_action.py create mode 100644 src/workos/radar/models/radar_list_entry_already_present_response.py create mode 100644 src/workos/radar/models/radar_standalone_assess_request.py create mode 100644 src/workos/radar/models/radar_standalone_delete_radar_list_entry_request.py create mode 100644 src/workos/radar/models/radar_standalone_response.py create mode 100644 src/workos/radar/models/radar_standalone_update_radar_attempt_request.py create mode 100644 src/workos/radar/models/radar_standalone_update_radar_list_request.py create mode 100644 src/workos/radar/models/radar_type.py create mode 100644 src/workos/sso/__init__.py create mode 100644 src/workos/sso/_resource.py create mode 100644 src/workos/sso/models/__init__.py create mode 100644 src/workos/sso/models/profile.py create mode 100644 src/workos/sso/models/sso_authorize_url_response.py create mode 100644 src/workos/sso/models/sso_logout_authorize_request.py create mode 100644 src/workos/sso/models/sso_logout_authorize_response.py create mode 100644 src/workos/sso/models/sso_provider.py create mode 100644 src/workos/sso/models/sso_token_response.py create mode 100644 src/workos/sso/models/sso_token_response_oauth_token.py create mode 100644 src/workos/sso/models/token_query.py create mode 100644 src/workos/types/__init__.py create mode 100644 src/workos/types/admin_portal/__init__.py create mode 100644 src/workos/types/api_keys/__init__.py create mode 100644 src/workos/types/application_client_secrets/__init__.py create mode 100644 src/workos/types/applications/__init__.py create mode 100644 src/workos/types/audit_logs/__init__.py create mode 100644 src/workos/types/authorization/__init__.py create mode 100644 src/workos/types/connect/__init__.py create mode 100644 src/workos/types/connections/__init__.py create mode 100644 src/workos/types/directories/__init__.py create mode 100644 src/workos/types/directory_groups/__init__.py create mode 100644 src/workos/types/directory_sync/__init__.py create mode 100644 src/workos/types/directory_users/__init__.py create mode 100644 src/workos/types/events/__init__.py create mode 100644 src/workos/types/feature_flags/__init__.py create mode 100644 src/workos/types/feature_flags/feature_flag.py create mode 100644 src/workos/types/feature_flags/feature_flag_owner.py create mode 100644 src/workos/types/feature_flags/flag.py create mode 100644 src/workos/types/feature_flags/flag_owner.py create mode 100644 src/workos/types/feature_flags_targets/__init__.py create mode 100644 src/workos/types/fga/__init__.py create mode 100644 src/workos/types/mfa/__init__.py create mode 100644 src/workos/types/multi_factor_auth/__init__.py create mode 100644 src/workos/types/multi_factor_auth/authentication_challenge.py create mode 100644 src/workos/types/multi_factor_auth/authentication_challenge_verify_response.py create mode 100644 src/workos/types/multi_factor_auth/authentication_challenges_verify_request.py create mode 100644 src/workos/types/multi_factor_auth/authentication_factor.py create mode 100644 src/workos/types/multi_factor_auth/authentication_factor_enrolled.py create mode 100644 src/workos/types/multi_factor_auth/authentication_factor_enrolled_sms.py create mode 100644 src/workos/types/multi_factor_auth/authentication_factor_enrolled_totp.py create mode 100644 src/workos/types/multi_factor_auth/authentication_factor_sms.py create mode 100644 src/workos/types/multi_factor_auth/authentication_factor_totp.py create mode 100644 src/workos/types/multi_factor_auth/authentication_factors_create_request.py create mode 100644 src/workos/types/multi_factor_auth/challenge_authentication_factor.py create mode 100644 src/workos/types/multi_factor_auth_challenges/__init__.py create mode 100644 src/workos/types/organization_domains/__init__.py create mode 100644 src/workos/types/organizations/__init__.py create mode 100644 src/workos/types/organizations/api_key_with_value.py create mode 100644 src/workos/types/organizations/api_key_with_value_owner.py create mode 100644 src/workos/types/organizations/audit_log_configuration.py create mode 100644 src/workos/types/organizations/audit_log_configuration_log_stream.py create mode 100644 src/workos/types/organizations/audit_logs_retention_json.py create mode 100644 src/workos/types/organizations/create_organization_api_key.py create mode 100644 src/workos/types/organizations/organization.py create mode 100644 src/workos/types/organizations/organization_domain_data.py create mode 100644 src/workos/types/organizations/organization_dto.py create mode 100644 src/workos/types/organizations/update_audit_logs_retention.py create mode 100644 src/workos/types/organizations/update_organization.py create mode 100644 src/workos/types/organizations_api_keys/__init__.py create mode 100644 src/workos/types/organizations_feature_flags/__init__.py create mode 100644 src/workos/types/permissions/__init__.py create mode 100644 src/workos/types/pipes/__init__.py create mode 100644 src/workos/types/portal/__init__.py create mode 100644 src/workos/types/radar/__init__.py create mode 100644 src/workos/types/sso/__init__.py create mode 100644 src/workos/types/user_management/__init__.py create mode 100644 src/workos/types/user_management/authenticate_response.py create mode 100644 src/workos/types/user_management/authenticate_response_impersonator.py create mode 100644 src/workos/types/user_management/authenticate_response_oauth_token.py create mode 100644 src/workos/types/user_management/authorization_code_session_authenticate_request.py create mode 100644 src/workos/types/user_management/connected_account.py create mode 100644 src/workos/types/user_management/cors_origin_response.py create mode 100644 src/workos/types/user_management/create_cors_origin.py create mode 100644 src/workos/types/user_management/create_magic_code_and_return.py create mode 100644 src/workos/types/user_management/create_password_reset.py create mode 100644 src/workos/types/user_management/create_password_reset_token.py create mode 100644 src/workos/types/user_management/create_redirect_uri.py create mode 100644 src/workos/types/user_management/create_user.py create mode 100644 src/workos/types/user_management/create_user_invite_options.py create mode 100644 src/workos/types/user_management/create_user_organization_membership.py create mode 100644 src/workos/types/user_management/data_integrations_list_response.py create mode 100644 src/workos/types/user_management/data_integrations_list_response_data.py create mode 100644 src/workos/types/user_management/data_integrations_list_response_data_connected_account.py create mode 100644 src/workos/types/user_management/device_authorization_response.py create mode 100644 src/workos/types/user_management/email_verification.py create mode 100644 src/workos/types/user_management/enroll_user_authentication_factor.py create mode 100644 src/workos/types/user_management/invitation.py create mode 100644 src/workos/types/user_management/jwks_response.py create mode 100644 src/workos/types/user_management/jwks_response_keys.py create mode 100644 src/workos/types/user_management/jwt_template_response.py create mode 100644 src/workos/types/user_management/magic_auth.py create mode 100644 src/workos/types/user_management/organization_membership.py create mode 100644 src/workos/types/user_management/password_reset.py create mode 100644 src/workos/types/user_management/password_session_authenticate_request.py create mode 100644 src/workos/types/user_management/redirect_uri.py create mode 100644 src/workos/types/user_management/refresh_token_session_authenticate_request.py create mode 100644 src/workos/types/user_management/resend_user_invite_options.py create mode 100644 src/workos/types/user_management/reset_password_response.py create mode 100644 src/workos/types/user_management/revoke_session.py create mode 100644 src/workos/types/user_management/send_verification_email_response.py create mode 100644 src/workos/types/user_management/sso_device_authorization_request.py create mode 100644 src/workos/types/user_management/update_jwt_template.py create mode 100644 src/workos/types/user_management/update_user.py create mode 100644 src/workos/types/user_management/update_user_organization_membership.py create mode 100644 src/workos/types/user_management/urn_ietf_params_oauth_grant_type_device_code_session_authenticate_request.py create mode 100644 src/workos/types/user_management/urn_workos_oauth_grant_type_email_verification_code_session_authenticate_request.py create mode 100644 src/workos/types/user_management/urn_workos_oauth_grant_type_magic_auth_code_session_authenticate_request.py create mode 100644 src/workos/types/user_management/urn_workos_oauth_grant_type_mfa_totp_session_authenticate_request.py create mode 100644 src/workos/types/user_management/urn_workos_oauth_grant_type_organization_selection_session_authenticate_request.py create mode 100644 src/workos/types/user_management/user.py create mode 100644 src/workos/types/user_management/user_authentication_factor_enroll_response.py create mode 100644 src/workos/types/user_management/user_identities_get_item.py create mode 100644 src/workos/types/user_management/user_invite.py create mode 100644 src/workos/types/user_management/user_organization_membership.py create mode 100644 src/workos/types/user_management/user_sessions_impersonator.py create mode 100644 src/workos/types/user_management/user_sessions_list_item.py create mode 100644 src/workos/types/user_management/verify_email_address.py create mode 100644 src/workos/types/user_management/verify_email_response.py create mode 100644 src/workos/types/user_management_authentication/__init__.py create mode 100644 src/workos/types/user_management_cors_origins/__init__.py create mode 100644 src/workos/types/user_management_data_providers/__init__.py create mode 100644 src/workos/types/user_management_invitations/__init__.py create mode 100644 src/workos/types/user_management_jwt_template/__init__.py create mode 100644 src/workos/types/user_management_magic_auth/__init__.py create mode 100644 src/workos/types/user_management_multi_factor_authentication/__init__.py create mode 100644 src/workos/types/user_management_organization_membership/__init__.py create mode 100644 src/workos/types/user_management_redirect_uris/__init__.py create mode 100644 src/workos/types/user_management_session_tokens/__init__.py create mode 100644 src/workos/types/user_management_users/__init__.py create mode 100644 src/workos/types/user_management_users/authorized_connect_application_list_data.py create mode 100644 src/workos/types/user_management_users_authorized_applications/__init__.py create mode 100644 src/workos/types/user_management_users_feature_flags/__init__.py create mode 100644 src/workos/types/webhooks/__init__.py create mode 100644 src/workos/types/widgets/__init__.py create mode 100644 src/workos/types/workos_connect/__init__.py create mode 100644 src/workos/user_management/__init__.py create mode 100644 src/workos/user_management/authentication/__init__.py create mode 100644 src/workos/user_management/authentication/_resource.py create mode 100644 src/workos/user_management/authentication/models/__init__.py create mode 100644 src/workos/user_management/authentication/models/authenticate_response.py create mode 100644 src/workos/user_management/authentication/models/authenticate_response_impersonator.py create mode 100644 src/workos/user_management/authentication/models/authenticate_response_oauth_token.py create mode 100644 src/workos/user_management/authentication/models/authorization_code_session_authenticate_request.py create mode 100644 src/workos/user_management/authentication/models/device_authorization_response.py create mode 100644 src/workos/user_management/authentication/models/password_session_authenticate_request.py create mode 100644 src/workos/user_management/authentication/models/refresh_token_session_authenticate_request.py create mode 100644 src/workos/user_management/authentication/models/revoke_session.py create mode 100644 src/workos/user_management/authentication/models/screen_hint_type.py create mode 100644 src/workos/user_management/authentication/models/sso_device_authorization_request.py create mode 100644 src/workos/user_management/authentication/models/urn_ietf_params_oauth_grant_type_device_code_session_authenticate_request.py create mode 100644 src/workos/user_management/authentication/models/urn_workos_oauth_grant_type_email_verification_code_session_authenticate_request.py create mode 100644 src/workos/user_management/authentication/models/urn_workos_oauth_grant_type_magic_auth_code_session_authenticate_request.py create mode 100644 src/workos/user_management/authentication/models/urn_workos_oauth_grant_type_mfa_totp_session_authenticate_request.py create mode 100644 src/workos/user_management/authentication/models/urn_workos_oauth_grant_type_organization_selection_session_authenticate_request.py create mode 100644 src/workos/user_management/authentication/models/user.py create mode 100644 src/workos/user_management/authentication/models/user_management_authentication_provider.py create mode 100644 src/workos/user_management/authentication/models/user_management_authentication_screen_hint.py create mode 100644 src/workos/user_management/cors_origins/__init__.py create mode 100644 src/workos/user_management/cors_origins/_resource.py create mode 100644 src/workos/user_management/cors_origins/models/__init__.py create mode 100644 src/workos/user_management/cors_origins/models/cors_origin_response.py create mode 100644 src/workos/user_management/cors_origins/models/create_cors_origin.py create mode 100644 src/workos/user_management/data_providers/__init__.py create mode 100644 src/workos/user_management/data_providers/_resource.py create mode 100644 src/workos/user_management/data_providers/models/__init__.py create mode 100644 src/workos/user_management/data_providers/models/connected_account.py create mode 100644 src/workos/user_management/data_providers/models/data_integrations_list_response.py create mode 100644 src/workos/user_management/data_providers/models/data_integrations_list_response_data.py create mode 100644 src/workos/user_management/data_providers/models/data_integrations_list_response_data_connected_account.py create mode 100644 src/workos/user_management/invitations/__init__.py create mode 100644 src/workos/user_management/invitations/_resource.py create mode 100644 src/workos/user_management/invitations/models/__init__.py create mode 100644 src/workos/user_management/invitations/models/create_user_invite_options.py create mode 100644 src/workos/user_management/invitations/models/invitation.py create mode 100644 src/workos/user_management/invitations/models/resend_user_invite_options.py create mode 100644 src/workos/user_management/invitations/models/user_invite.py create mode 100644 src/workos/user_management/invitations/models/user_management_invitations_order.py create mode 100644 src/workos/user_management/jwt_template/__init__.py create mode 100644 src/workos/user_management/jwt_template/_resource.py create mode 100644 src/workos/user_management/jwt_template/models/__init__.py create mode 100644 src/workos/user_management/jwt_template/models/jwt_template_response.py create mode 100644 src/workos/user_management/jwt_template/models/update_jwt_template.py create mode 100644 src/workos/user_management/magic_auth/__init__.py create mode 100644 src/workos/user_management/magic_auth/_resource.py create mode 100644 src/workos/user_management/magic_auth/models/__init__.py create mode 100644 src/workos/user_management/magic_auth/models/create_magic_code_and_return.py create mode 100644 src/workos/user_management/magic_auth/models/magic_auth.py create mode 100644 src/workos/user_management/multi_factor_authentication/__init__.py create mode 100644 src/workos/user_management/multi_factor_authentication/_resource.py create mode 100644 src/workos/user_management/multi_factor_authentication/models/__init__.py create mode 100644 src/workos/user_management/multi_factor_authentication/models/enroll_user_authentication_factor.py create mode 100644 src/workos/user_management/multi_factor_authentication/models/user_authentication_factor_enroll_response.py create mode 100644 src/workos/user_management/multi_factor_authentication/models/user_management_multi_factor_authentication_order.py create mode 100644 src/workos/user_management/organization_membership/__init__.py create mode 100644 src/workos/user_management/organization_membership/_resource.py create mode 100644 src/workos/user_management/organization_membership/models/__init__.py create mode 100644 src/workos/user_management/organization_membership/models/create_user_organization_membership.py create mode 100644 src/workos/user_management/organization_membership/models/organization_membership.py create mode 100644 src/workos/user_management/organization_membership/models/update_user_organization_membership.py create mode 100644 src/workos/user_management/organization_membership/models/user_management_organization_membership_order.py create mode 100644 src/workos/user_management/organization_membership/models/user_management_organization_membership_statuses.py create mode 100644 src/workos/user_management/organization_membership/models/user_organization_membership.py create mode 100644 src/workos/user_management/redirect_uris/__init__.py create mode 100644 src/workos/user_management/redirect_uris/_resource.py create mode 100644 src/workos/user_management/redirect_uris/models/__init__.py create mode 100644 src/workos/user_management/redirect_uris/models/create_redirect_uri.py create mode 100644 src/workos/user_management/redirect_uris/models/redirect_uri.py create mode 100644 src/workos/user_management/session_tokens/__init__.py create mode 100644 src/workos/user_management/session_tokens/_resource.py create mode 100644 src/workos/user_management/session_tokens/models/__init__.py create mode 100644 src/workos/user_management/session_tokens/models/jwks_response.py create mode 100644 src/workos/user_management/session_tokens/models/jwks_response_keys.py create mode 100644 src/workos/user_management/users/__init__.py create mode 100644 src/workos/user_management/users/_resource.py create mode 100644 src/workos/user_management/users/models/__init__.py create mode 100644 src/workos/user_management/users/models/create_password_reset.py create mode 100644 src/workos/user_management/users/models/create_password_reset_token.py create mode 100644 src/workos/user_management/users/models/create_user.py create mode 100644 src/workos/user_management/users/models/email_verification.py create mode 100644 src/workos/user_management/users/models/password_reset.py create mode 100644 src/workos/user_management/users/models/reset_password_response.py create mode 100644 src/workos/user_management/users/models/send_verification_email_response.py create mode 100644 src/workos/user_management/users/models/update_user.py create mode 100644 src/workos/user_management/users/models/user_identities_get_item.py create mode 100644 src/workos/user_management/users/models/user_management_users_order.py create mode 100644 src/workos/user_management/users/models/user_sessions_impersonator.py create mode 100644 src/workos/user_management/users/models/user_sessions_list_item.py create mode 100644 src/workos/user_management/users/models/verify_email_address.py create mode 100644 src/workos/user_management/users/models/verify_email_response.py create mode 100644 src/workos/user_management_users/__init__.py create mode 100644 src/workos/user_management_users/authorized_applications/__init__.py create mode 100644 src/workos/user_management_users/authorized_applications/_resource.py create mode 100644 src/workos/user_management_users/authorized_applications/models/__init__.py create mode 100644 src/workos/user_management_users/authorized_applications/models/authorized_connect_application_list_data.py create mode 100644 src/workos/user_management_users/authorized_applications/models/user_management_users_authorized_applications_order.py create mode 100644 src/workos/user_management_users/feature_flags/__init__.py create mode 100644 src/workos/user_management_users/feature_flags/_resource.py create mode 100644 src/workos/user_management_users/feature_flags/models/__init__.py create mode 100644 src/workos/user_management_users/feature_flags/models/user_management_users_feature_flags_order.py create mode 100644 src/workos/webhooks/__init__.py create mode 100644 src/workos/webhooks/_resource.py create mode 100644 src/workos/webhooks/models/__init__.py create mode 100644 src/workos/webhooks/models/create_webhook_endpoint.py create mode 100644 src/workos/webhooks/models/update_webhook_endpoint.py create mode 100644 src/workos/webhooks/models/webhook_endpoint_json.py create mode 100644 src/workos/webhooks/models/webhooks_order.py create mode 100644 src/workos/widgets/__init__.py create mode 100644 src/workos/widgets/_resource.py create mode 100644 src/workos/widgets/models/__init__.py create mode 100644 src/workos/widgets/models/widget_session_token.py create mode 100644 src/workos/widgets/models/widget_session_token_response.py create mode 100644 src/workos/workos_connect/__init__.py create mode 100644 src/workos/workos_connect/_resource.py create mode 100644 src/workos/workos_connect/models/__init__.py create mode 100644 src/workos/workos_connect/models/external_auth_complete_response.py create mode 100644 src/workos/workos_connect/models/user_consent_option.py create mode 100644 src/workos/workos_connect/models/user_consent_option_choice.py create mode 100644 src/workos/workos_connect/models/user_management_login_request.py create mode 100644 src/workos/workos_connect/models/user_object.py create mode 100644 tests/fixtures/add_role_permission.json create mode 100644 tests/fixtures/api_key.json create mode 100644 tests/fixtures/api_key_owner.json create mode 100644 tests/fixtures/api_key_validation_response.json create mode 100644 tests/fixtures/api_key_with_value.json create mode 100644 tests/fixtures/api_key_with_value_owner.json create mode 100644 tests/fixtures/application_credentials_list_item.json create mode 100644 tests/fixtures/assign_role.json create mode 100644 tests/fixtures/audit_log_action_json.json create mode 100644 tests/fixtures/audit_log_configuration.json create mode 100644 tests/fixtures/audit_log_configuration_log_stream.json create mode 100644 tests/fixtures/audit_log_event.json create mode 100644 tests/fixtures/audit_log_event_actor.json create mode 100644 tests/fixtures/audit_log_event_context.json create mode 100644 tests/fixtures/audit_log_event_create_response.json create mode 100644 tests/fixtures/audit_log_event_ingestion.json create mode 100644 tests/fixtures/audit_log_event_target.json create mode 100644 tests/fixtures/audit_log_export_creation.json create mode 100644 tests/fixtures/audit_log_export_json.json create mode 100644 tests/fixtures/audit_log_schema.json create mode 100644 tests/fixtures/audit_log_schema_actor.json create mode 100644 tests/fixtures/audit_log_schema_json.json create mode 100644 tests/fixtures/audit_log_schema_json_actor.json create mode 100644 tests/fixtures/audit_log_schema_json_target.json create mode 100644 tests/fixtures/audit_log_schema_target.json create mode 100644 tests/fixtures/audit_logs_retention_json.json create mode 100644 tests/fixtures/authenticate_response.json create mode 100644 tests/fixtures/authenticate_response_impersonator.json create mode 100644 tests/fixtures/authenticate_response_oauth_token.json create mode 100644 tests/fixtures/authentication_challenge.json create mode 100644 tests/fixtures/authentication_challenge_verify_response.json create mode 100644 tests/fixtures/authentication_challenges_verify_request.json create mode 100644 tests/fixtures/authentication_factor.json create mode 100644 tests/fixtures/authentication_factor_enrolled.json create mode 100644 tests/fixtures/authentication_factor_enrolled_sms.json create mode 100644 tests/fixtures/authentication_factor_enrolled_totp.json create mode 100644 tests/fixtures/authentication_factor_sms.json create mode 100644 tests/fixtures/authentication_factor_totp.json create mode 100644 tests/fixtures/authentication_factors_create_request.json create mode 100644 tests/fixtures/authorization_check.json create mode 100644 tests/fixtures/authorization_code_session_authenticate_request.json create mode 100644 tests/fixtures/authorization_permission.json create mode 100644 tests/fixtures/authorization_resource.json create mode 100644 tests/fixtures/authorized_connect_application_list_data.json create mode 100644 tests/fixtures/challenge_authentication_factor.json create mode 100644 tests/fixtures/check_authorization.json create mode 100644 tests/fixtures/connect_application.json create mode 100644 tests/fixtures/connected_account.json create mode 100644 tests/fixtures/connection.json create mode 100644 tests/fixtures/connection_domain.json create mode 100644 tests/fixtures/connection_option.json create mode 100644 tests/fixtures/cors_origin_response.json create mode 100644 tests/fixtures/create_application_secret.json create mode 100644 tests/fixtures/create_authorization_permission.json create mode 100644 tests/fixtures/create_authorization_resource.json create mode 100644 tests/fixtures/create_cors_origin.json create mode 100644 tests/fixtures/create_m2m_application.json create mode 100644 tests/fixtures/create_magic_code_and_return.json create mode 100644 tests/fixtures/create_oauth_application.json create mode 100644 tests/fixtures/create_organization_api_key.json create mode 100644 tests/fixtures/create_organization_domain.json create mode 100644 tests/fixtures/create_organization_role.json create mode 100644 tests/fixtures/create_password_reset.json create mode 100644 tests/fixtures/create_password_reset_token.json create mode 100644 tests/fixtures/create_redirect_uri.json create mode 100644 tests/fixtures/create_role.json create mode 100644 tests/fixtures/create_user.json create mode 100644 tests/fixtures/create_user_invite_options.json create mode 100644 tests/fixtures/create_user_organization_membership.json create mode 100644 tests/fixtures/create_webhook_endpoint.json create mode 100644 tests/fixtures/data_integration_access_token_response.json create mode 100644 tests/fixtures/data_integration_authorize_url_response.json create mode 100644 tests/fixtures/data_integrations_get_data_integration_authorize_url_request.json create mode 100644 tests/fixtures/data_integrations_get_user_token_request.json create mode 100644 tests/fixtures/data_integrations_list_response.json create mode 100644 tests/fixtures/data_integrations_list_response_data.json create mode 100644 tests/fixtures/data_integrations_list_response_data_connected_account.json create mode 100644 tests/fixtures/device_authorization_response.json create mode 100644 tests/fixtures/directory.json create mode 100644 tests/fixtures/directory_group.json create mode 100644 tests/fixtures/directory_metadata.json create mode 100644 tests/fixtures/directory_metadata_user.json create mode 100644 tests/fixtures/directory_user_with_groups.json create mode 100644 tests/fixtures/directory_user_with_groups_email.json create mode 100644 tests/fixtures/email_verification.json create mode 100644 tests/fixtures/enroll_user_authentication_factor.json create mode 100644 tests/fixtures/event.json create mode 100644 tests/fixtures/external_auth_complete_response.json create mode 100644 tests/fixtures/feature_flag.json create mode 100644 tests/fixtures/feature_flag_owner.json create mode 100644 tests/fixtures/flag.json create mode 100644 tests/fixtures/flag_owner.json create mode 100644 tests/fixtures/generate_link.json create mode 100644 tests/fixtures/intent_options.json create mode 100644 tests/fixtures/invitation.json create mode 100644 tests/fixtures/jwks_response.json create mode 100644 tests/fixtures/jwks_response_keys.json create mode 100644 tests/fixtures/jwt_template_response.json create mode 100644 tests/fixtures/list.json create mode 100644 tests/fixtures/list_api_key.json create mode 100644 tests/fixtures/list_audit_log_action_json.json create mode 100644 tests/fixtures/list_audit_log_schema_json.json create mode 100644 tests/fixtures/list_authentication_factor.json create mode 100644 tests/fixtures/list_authorization_permission.json create mode 100644 tests/fixtures/list_authorization_resource.json create mode 100644 tests/fixtures/list_authorized_connect_application_list_data.json create mode 100644 tests/fixtures/list_connect_application.json create mode 100644 tests/fixtures/list_connection.json create mode 100644 tests/fixtures/list_data.json create mode 100644 tests/fixtures/list_directory.json create mode 100644 tests/fixtures/list_directory_group.json create mode 100644 tests/fixtures/list_directory_user_with_groups.json create mode 100644 tests/fixtures/list_event.json create mode 100644 tests/fixtures/list_flag.json create mode 100644 tests/fixtures/list_organization.json create mode 100644 tests/fixtures/list_role_assignment.json create mode 100644 tests/fixtures/list_user.json create mode 100644 tests/fixtures/list_user_invite.json create mode 100644 tests/fixtures/list_user_organization_membership.json create mode 100644 tests/fixtures/list_user_organization_membership_base_list_data.json create mode 100644 tests/fixtures/list_user_sessions_list_item.json create mode 100644 tests/fixtures/list_webhook_endpoint_json.json create mode 100644 tests/fixtures/magic_auth.json create mode 100644 tests/fixtures/new_connect_application_secret.json create mode 100644 tests/fixtures/organization.json create mode 100644 tests/fixtures/organization_domain.json create mode 100644 tests/fixtures/organization_domain_data.json create mode 100644 tests/fixtures/organization_domain_stand_alone.json create mode 100644 tests/fixtures/organization_dto.json create mode 100644 tests/fixtures/organization_membership.json create mode 100644 tests/fixtures/password_reset.json create mode 100644 tests/fixtures/password_session_authenticate_request.json create mode 100644 tests/fixtures/permission.json create mode 100644 tests/fixtures/portal_link_response.json create mode 100644 tests/fixtures/profile.json create mode 100644 tests/fixtures/radar_list_entry_already_present_response.json create mode 100644 tests/fixtures/radar_standalone_assess_request.json create mode 100644 tests/fixtures/radar_standalone_delete_radar_list_entry_request.json create mode 100644 tests/fixtures/radar_standalone_response.json create mode 100644 tests/fixtures/radar_standalone_update_radar_attempt_request.json create mode 100644 tests/fixtures/radar_standalone_update_radar_list_request.json create mode 100644 tests/fixtures/redirect_uri.json create mode 100644 tests/fixtures/redirect_uri_dto.json create mode 100644 tests/fixtures/refresh_token_session_authenticate_request.json create mode 100644 tests/fixtures/remove_role.json create mode 100644 tests/fixtures/resend_user_invite_options.json create mode 100644 tests/fixtures/reset_password_response.json create mode 100644 tests/fixtures/revoke_session.json create mode 100644 tests/fixtures/role.json create mode 100644 tests/fixtures/role_assignment.json create mode 100644 tests/fixtures/role_assignment_resource.json create mode 100644 tests/fixtures/role_list.json create mode 100644 tests/fixtures/send_verification_email_response.json create mode 100644 tests/fixtures/set_role_permissions.json create mode 100644 tests/fixtures/slim_role.json create mode 100644 tests/fixtures/sso_authorize_url_response.json create mode 100644 tests/fixtures/sso_device_authorization_request.json create mode 100644 tests/fixtures/sso_intent_options.json create mode 100644 tests/fixtures/sso_logout_authorize_request.json create mode 100644 tests/fixtures/sso_logout_authorize_response.json create mode 100644 tests/fixtures/sso_token_response.json create mode 100644 tests/fixtures/sso_token_response_oauth_token.json create mode 100644 tests/fixtures/token_query.json create mode 100644 tests/fixtures/update_audit_logs_retention.json create mode 100644 tests/fixtures/update_authorization_permission.json create mode 100644 tests/fixtures/update_authorization_resource.json create mode 100644 tests/fixtures/update_jwt_template.json create mode 100644 tests/fixtures/update_oauth_application.json create mode 100644 tests/fixtures/update_organization.json create mode 100644 tests/fixtures/update_organization_role.json create mode 100644 tests/fixtures/update_role.json create mode 100644 tests/fixtures/update_user.json create mode 100644 tests/fixtures/update_user_organization_membership.json create mode 100644 tests/fixtures/update_webhook_endpoint.json create mode 100644 tests/fixtures/urn_ietf_params_oauth_grant_type_device_code_session_authenticate_request.json create mode 100644 tests/fixtures/urn_workos_oauth_grant_type_email_verification_code_session_authenticate_request.json create mode 100644 tests/fixtures/urn_workos_oauth_grant_type_magic_auth_code_session_authenticate_request.json create mode 100644 tests/fixtures/urn_workos_oauth_grant_type_mfa_totp_session_authenticate_request.json create mode 100644 tests/fixtures/urn_workos_oauth_grant_type_organization_selection_session_authenticate_request.json create mode 100644 tests/fixtures/user.json create mode 100644 tests/fixtures/user_authentication_factor_enroll_response.json create mode 100644 tests/fixtures/user_consent_option.json create mode 100644 tests/fixtures/user_consent_option_choice.json create mode 100644 tests/fixtures/user_identities_get_item.json create mode 100644 tests/fixtures/user_invite.json create mode 100644 tests/fixtures/user_management_login_request.json create mode 100644 tests/fixtures/user_object.json create mode 100644 tests/fixtures/user_organization_membership.json create mode 100644 tests/fixtures/user_organization_membership_base_list_data.json create mode 100644 tests/fixtures/user_sessions_impersonator.json create mode 100644 tests/fixtures/user_sessions_list_item.json create mode 100644 tests/fixtures/validate_api_key.json create mode 100644 tests/fixtures/verify_email_address.json create mode 100644 tests/fixtures/verify_email_response.json create mode 100644 tests/fixtures/webhook_endpoint_json.json create mode 100644 tests/fixtures/widget_session_token.json create mode 100644 tests/fixtures/widget_session_token_response.json create mode 100644 tests/generated_helpers.py create mode 100644 tests/test_admin_portal.py create mode 100644 tests/test_api_keys.py create mode 100644 tests/test_application_client_secrets.py create mode 100644 tests/test_applications.py create mode 100644 tests/test_audit_logs.py create mode 100644 tests/test_authorization.py create mode 100644 tests/test_connections.py create mode 100644 tests/test_directories.py create mode 100644 tests/test_directory_groups.py create mode 100644 tests/test_directory_users.py create mode 100644 tests/test_events.py create mode 100644 tests/test_feature_flags.py create mode 100644 tests/test_feature_flags_targets.py create mode 100644 tests/test_generated_client.py create mode 100644 tests/test_models_round_trip.py create mode 100644 tests/test_multi_factor_auth.py create mode 100644 tests/test_multi_factor_auth_challenges.py create mode 100644 tests/test_organization_domains.py create mode 100644 tests/test_organizations.py create mode 100644 tests/test_organizations_api_keys.py create mode 100644 tests/test_organizations_feature_flags.py create mode 100644 tests/test_pagination.py create mode 100644 tests/test_permissions.py create mode 100644 tests/test_pipes.py create mode 100644 tests/test_radar.py create mode 100644 tests/test_sso.py create mode 100644 tests/test_user_management_authentication.py create mode 100644 tests/test_user_management_cors_origins.py create mode 100644 tests/test_user_management_data_providers.py create mode 100644 tests/test_user_management_invitations.py create mode 100644 tests/test_user_management_jwt_template.py create mode 100644 tests/test_user_management_magic_auth.py create mode 100644 tests/test_user_management_multi_factor_authentication.py create mode 100644 tests/test_user_management_organization_membership.py create mode 100644 tests/test_user_management_redirect_uris.py create mode 100644 tests/test_user_management_session_tokens.py create mode 100644 tests/test_user_management_users.py create mode 100644 tests/test_user_management_users_authorized_applications.py create mode 100644 tests/test_user_management_users_feature_flags.py create mode 100644 tests/test_webhooks.py create mode 100644 tests/test_widgets.py create mode 100644 tests/test_workos_connect.py diff --git a/src/workos/__init__.py b/src/workos/__init__.py index ab4133bb..f1994616 100644 --- a/src/workos/__init__.py +++ b/src/workos/__init__.py @@ -1,45 +1,48 @@ +# This file is auto-generated by oagen. Do not edit. + """WorkOS Python SDK.""" -from ._client import AsyncWorkOS, WorkOS +from ._client import AsyncWorkOSClient, WorkOSClient from ._errors import ( - WorkOSError, - AuthenticationError, - BadRequestError, - ConflictError, - ConfigurationError, - ForbiddenError, - NotFoundError, - RateLimitExceededError, - ServerError, - UnprocessableEntityError, - WorkOSConnectionError, - WorkOSTimeoutError, + BaseRequestException, + AuthenticationException, + BadRequestException, + ConflictException, + ConfigurationException, + AuthorizationException, + NotFoundException, + RateLimitExceededException, + ServerException, + UnprocessableEntityException, + WorkOSConnectionException, + WorkOSTimeoutException, ) -from ._pagination import AsyncPage, SyncPage +from ._pagination import AsyncPage, SyncPage, WorkOSListResource from ._types import RequestOptions # Backward-compatible aliases -WorkOSClient = WorkOS -AsyncWorkOSClient = AsyncWorkOS +WorkOS = WorkOSClient +AsyncWorkOS = AsyncWorkOSClient __all__ = [ - "AsyncWorkOS", + "WorkOSClient", "AsyncWorkOSClient", "WorkOS", - "WorkOSClient", + "AsyncWorkOS", "RequestOptions", - "WorkOSError", - "AuthenticationError", - "BadRequestError", - "ConflictError", - "ConfigurationError", - "ForbiddenError", - "NotFoundError", - "RateLimitExceededError", - "ServerError", - "UnprocessableEntityError", - "WorkOSConnectionError", - "WorkOSTimeoutError", + "BaseRequestException", + "AuthenticationException", + "BadRequestException", + "ConflictException", + "ConfigurationException", + "AuthorizationException", + "NotFoundException", + "RateLimitExceededException", + "ServerException", + "UnprocessableEntityException", + "WorkOSConnectionException", + "WorkOSTimeoutException", "AsyncPage", "SyncPage", + "WorkOSListResource", ] diff --git a/src/workos/_client.py b/src/workos/_client.py index f7ff3341..14ea0fda 100644 --- a/src/workos/_client.py +++ b/src/workos/_client.py @@ -11,7 +11,7 @@ import random from datetime import datetime, timezone from email.utils import parsedate_to_datetime -from typing import Any, Dict, Optional, Type, cast, overload +from typing import Any, Dict, List, Literal, Optional, Type, cast, overload import httpx @@ -121,6 +121,16 @@ from .webhooks._resource import Webhooks, AsyncWebhooks from .widgets._resource import Widgets, AsyncWidgets from .audit_logs._resource import AuditLogs, AsyncAuditLogs +from .session import AsyncSession, Session +from .directory_users.models import DirectoryUsersOrder +from .directory_groups.models import DirectoryGroupsOrder +from .directories.models import DirectoriesOrder +from .user_management.users.models import UserManagementUsersOrder +from .user_management.authentication.models import ( + UserManagementAuthenticationProvider, + UserManagementAuthenticationScreenHint, +) +from .common.models import CreateUserDtoPasswordHashType, UpdateUserDtoPasswordHashType try: from importlib.metadata import version as _pkg_version @@ -223,6 +233,140 @@ def data_providers(self) -> UserManagementDataProviders: def multi_factor_authentication(self) -> UserManagementMultiFactorAuthentication: return UserManagementMultiFactorAuthentication(self._client) + def load_sealed_session( + self, *, sealed_session: str, cookie_password: str + ) -> Session: + return Session( + client=self._client, + session_data=sealed_session, + cookie_password=cookie_password, + ) + + def get_user(self, user_id: str): + return self.users.get_user(user_id) + + def get_user_by_external_id(self, external_id: str): + return self.users.get_by_external_id(external_id) + + def list_users( + self, + *, + email: Optional[str] = None, + organization_id: Optional[str] = None, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[UserManagementUsersOrder] = None, + ): + return self.users.list_users( + email=email, + organization_id=organization_id, + limit=limit, + before=before, + after=after, + order=order, + ) + + def create_user( + self, + *, + email: str, + password: Optional[str] = None, + password_hash: Optional[str] = None, + password_hash_type: Optional[CreateUserDtoPasswordHashType] = None, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + email_verified: Optional[bool] = None, + external_id: Optional[str] = None, + metadata: Optional[Dict[str, str]] = None, + ): + return self.users.create( + email=email, + password=password, + password_hash=password_hash, + password_hash_type=password_hash_type, + first_name=first_name, + last_name=last_name, + email_verified=email_verified, + external_id=external_id, + metadata=metadata, + ) + + def update_user( + self, + *, + user_id: str, + email: Optional[str] = None, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + email_verified: Optional[bool] = None, + password: Optional[str] = None, + password_hash: Optional[str] = None, + password_hash_type: Optional[UpdateUserDtoPasswordHashType] = None, + external_id: Optional[str] = None, + metadata: Optional[Dict[str, str]] = None, + locale: Optional[str] = None, + ): + return self.users.update( + user_id, + email=email, + first_name=first_name, + last_name=last_name, + email_verified=email_verified, + password=password, + password_hash=password_hash, + password_hash_type=password_hash_type, + external_id=external_id, + metadata=metadata, + locale=locale, + ) + + def delete_user(self, user_id: str) -> None: + self.users.delete(user_id) + + def get_authorization_url( + self, + *, + redirect_uri: str, + domain_hint: Optional[str] = None, + login_hint: Optional[str] = None, + state: Optional[str] = None, + provider: Optional[UserManagementAuthenticationProvider] = None, + connection_id: Optional[str] = None, + organization_id: Optional[str] = None, + code_challenge: Optional[str] = None, + code_challenge_method: Optional[Literal["S256"]] = None, + provider_query_params: Optional[Dict[str, str]] = None, + provider_scopes: Optional[List[str]] = None, + invitation_token: Optional[str] = None, + screen_hint: Optional[UserManagementAuthenticationScreenHint] = None, + prompt: Optional[str] = None, + ) -> str: + return self.authentication.authorize( + redirect_uri=redirect_uri, + client_id=self._client.client_id or "", + response_type="code", + domain_hint=domain_hint, + login_hint=login_hint, + state=state, + provider=provider, + connection_id=connection_id, + organization_id=organization_id, + code_challenge=code_challenge, + code_challenge_method=code_challenge_method, + provider_query_params=provider_query_params, + provider_scopes=provider_scopes, + invitation_token=invitation_token, + screen_hint=screen_hint, + prompt=prompt, + ) + + def get_jwks_url(self) -> str: + return self._client.build_url(f"sso/jwks/{self._client.client_id}") + + def get_logout_url(self, session_id: str, return_to: Optional[str] = None) -> str: + return self.authentication.logout(session_id=session_id, return_to=return_to) + class UserManagementUsersNamespace(object): """UserManagementUsers resources.""" @@ -239,6 +383,84 @@ def authorized_applications(self) -> UserManagementUsersAuthorizedApplications: return UserManagementUsersAuthorizedApplications(self._client) +class DirectorySyncNamespace(object): + """Directory Sync compatibility resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def list_users( + self, + *, + directory_id: Optional[str] = None, + group_id: Optional[str] = None, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[DirectoryUsersOrder] = None, + ): + return self._client.directory_users.list( + directory=directory_id, + group=group_id, + limit=limit, + before=before, + after=after, + order=order, + ) + + def list_groups( + self, + *, + directory_id: Optional[str] = None, + user_id: Optional[str] = None, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[DirectoryGroupsOrder] = None, + ): + return self._client.directory_groups.list( + directory=directory_id, + user=user_id, + limit=limit, + before=before, + after=after, + order=order, + ) + + def list_directories( + self, + *, + search: Optional[str] = None, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + organization_id: Optional[str] = None, + order: Optional[DirectoriesOrder] = None, + domain: Optional[str] = None, + ): + return self._client.directories.list( + search=search, + limit=limit, + before=before, + after=after, + organization_id=organization_id, + order=order, + domain=domain, + ) + + def get_user(self, user_id: str): + return self._client.directory_users.get(user_id) + + def get_group(self, group_id: str): + return self._client.directory_groups.get(group_id) + + def get_directory(self, directory_id: str): + return self._client.directories.get(directory_id) + + def delete_directory(self, directory_id: str) -> None: + self._client.directories.delete(directory_id) + + class AsyncMultiFactorAuthNamespace(AsyncMultiFactorAuth): """MultiFactorAuth resources (async).""" @@ -328,6 +550,144 @@ def multi_factor_authentication( ) -> AsyncUserManagementMultiFactorAuthentication: return AsyncUserManagementMultiFactorAuthentication(self._client) + def load_sealed_session( + self, *, sealed_session: str, cookie_password: str + ) -> AsyncSession: + return AsyncSession( + client=self._client, + session_data=sealed_session, + cookie_password=cookie_password, + ) + + async def get_user(self, user_id: str): + return await self.users.get_user(user_id) + + async def get_user_by_external_id(self, external_id: str): + return await self.users.get_by_external_id(external_id) + + async def list_users( + self, + *, + email: Optional[str] = None, + organization_id: Optional[str] = None, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[UserManagementUsersOrder] = None, + ): + return await self.users.list_users( + email=email, + organization_id=organization_id, + limit=limit, + before=before, + after=after, + order=order, + ) + + async def create_user( + self, + *, + email: str, + password: Optional[str] = None, + password_hash: Optional[str] = None, + password_hash_type: Optional[CreateUserDtoPasswordHashType] = None, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + email_verified: Optional[bool] = None, + external_id: Optional[str] = None, + metadata: Optional[Dict[str, str]] = None, + ): + return await self.users.create( + email=email, + password=password, + password_hash=password_hash, + password_hash_type=password_hash_type, + first_name=first_name, + last_name=last_name, + email_verified=email_verified, + external_id=external_id, + metadata=metadata, + ) + + async def update_user( + self, + *, + user_id: str, + email: Optional[str] = None, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + email_verified: Optional[bool] = None, + password: Optional[str] = None, + password_hash: Optional[str] = None, + password_hash_type: Optional[UpdateUserDtoPasswordHashType] = None, + external_id: Optional[str] = None, + metadata: Optional[Dict[str, str]] = None, + locale: Optional[str] = None, + ): + return await self.users.update( + user_id, + email=email, + first_name=first_name, + last_name=last_name, + email_verified=email_verified, + password=password, + password_hash=password_hash, + password_hash_type=password_hash_type, + external_id=external_id, + metadata=metadata, + locale=locale, + ) + + async def delete_user(self, user_id: str) -> None: + await self.users.delete(user_id) + + async def get_authorization_url( + self, + *, + redirect_uri: str, + domain_hint: Optional[str] = None, + login_hint: Optional[str] = None, + state: Optional[str] = None, + provider: Optional[UserManagementAuthenticationProvider] = None, + connection_id: Optional[str] = None, + organization_id: Optional[str] = None, + code_challenge: Optional[str] = None, + code_challenge_method: Optional[Literal["S256"]] = None, + provider_query_params: Optional[Dict[str, str]] = None, + provider_scopes: Optional[List[str]] = None, + invitation_token: Optional[str] = None, + screen_hint: Optional[UserManagementAuthenticationScreenHint] = None, + prompt: Optional[str] = None, + ) -> str: + return await self.authentication.authorize( + redirect_uri=redirect_uri, + client_id=self._client.client_id or "", + response_type="code", + domain_hint=domain_hint, + login_hint=login_hint, + state=state, + provider=provider, + connection_id=connection_id, + organization_id=organization_id, + code_challenge=code_challenge, + code_challenge_method=code_challenge_method, + provider_query_params=provider_query_params, + provider_scopes=provider_scopes, + invitation_token=invitation_token, + screen_hint=screen_hint, + prompt=prompt, + ) + + def get_jwks_url(self) -> str: + return self._client.build_url(f"sso/jwks/{self._client.client_id}") + + async def get_logout_url( + self, session_id: str, return_to: Optional[str] = None + ) -> str: + return await self.authentication.logout( + session_id=session_id, return_to=return_to + ) + class AsyncUserManagementUsersNamespace(object): """UserManagementUsers resources (async).""" @@ -344,6 +704,84 @@ def authorized_applications(self) -> AsyncUserManagementUsersAuthorizedApplicati return AsyncUserManagementUsersAuthorizedApplications(self._client) +class AsyncDirectorySyncNamespace(object): + """Directory Sync compatibility resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def list_users( + self, + *, + directory_id: Optional[str] = None, + group_id: Optional[str] = None, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[DirectoryUsersOrder] = None, + ): + return await self._client.directory_users.list( + directory=directory_id, + group=group_id, + limit=limit, + before=before, + after=after, + order=order, + ) + + async def list_groups( + self, + *, + directory_id: Optional[str] = None, + user_id: Optional[str] = None, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[DirectoryGroupsOrder] = None, + ): + return await self._client.directory_groups.list( + directory=directory_id, + user=user_id, + limit=limit, + before=before, + after=after, + order=order, + ) + + async def list_directories( + self, + *, + search: Optional[str] = None, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + organization_id: Optional[str] = None, + order: Optional[DirectoriesOrder] = None, + domain: Optional[str] = None, + ): + return await self._client.directories.list( + search=search, + limit=limit, + before=before, + after=after, + organization_id=organization_id, + order=order, + domain=domain, + ) + + async def get_user(self, user_id: str): + return await self._client.directory_users.get(user_id) + + async def get_group(self, group_id: str): + return await self._client.directory_groups.get(group_id) + + async def get_directory(self, directory_id: str): + return await self._client.directories.get(directory_id) + + async def delete_directory(self, directory_id: str) -> None: + await self._client.directories.delete(directory_id) + + class _BaseWorkOSClient: """Shared WorkOS client implementation.""" @@ -352,8 +790,9 @@ def __init__( *, api_key: Optional[str] = None, client_id: Optional[str] = None, - base_url: str = "https://api.workos.com", - request_timeout: int = 25, + base_url: Optional[str] = None, + request_timeout: Optional[int] = None, + jwt_leeway: float = 0.0, max_retries: int = MAX_RETRIES, ) -> None: self._api_key = api_key or os.environ.get("WORKOS_API_KEY") @@ -368,11 +807,18 @@ def __init__( "WorkOS client ID must be provided when instantiating the client " "or via the WORKOS_CLIENT_ID environment variable." ) + resolved_base_url = base_url or os.environ.get( + "WORKOS_BASE_URL", "https://api.workos.com" + ) # Ensure base_url has a trailing slash for backward compatibility - self._base_url = base_url.rstrip("/") + "/" - self._request_timeout = request_timeout + self._base_url = resolved_base_url.rstrip("/") + "/" + self._request_timeout = ( + request_timeout + if request_timeout is not None + else int(os.environ.get("WORKOS_REQUEST_TIMEOUT", "25")) + ) self._max_retries = max_retries - self._jwt_leeway: float = 0.0 + self._jwt_leeway = jwt_leeway @property def base_url(self) -> str: @@ -554,8 +1000,9 @@ def __init__( *, api_key: Optional[str] = None, client_id: Optional[str] = None, - base_url: str = "https://api.workos.com", - request_timeout: int = 25, + base_url: Optional[str] = None, + request_timeout: Optional[int] = None, + jwt_leeway: float = 0.0, max_retries: int = MAX_RETRIES, ) -> None: """Initialize the WorkOS client. @@ -563,8 +1010,9 @@ def __init__( Args: api_key: WorkOS API key. Falls back to the WORKOS_API_KEY environment variable. client_id: WorkOS client ID. Falls back to the WORKOS_CLIENT_ID environment variable. - base_url: Base URL for API requests. Defaults to "https://api.workos.com". - request_timeout: HTTP request timeout in seconds. Defaults to 25. + base_url: Base URL for API requests. Falls back to WORKOS_BASE_URL or "https://api.workos.com". + request_timeout: HTTP request timeout in seconds. Falls back to WORKOS_REQUEST_TIMEOUT or 25. + jwt_leeway: JWT clock skew leeway in seconds. max_retries: Maximum number of retries for failed requests. Defaults to 3. Raises: @@ -575,9 +1023,12 @@ def __init__( client_id=client_id, base_url=base_url, request_timeout=request_timeout, + jwt_leeway=jwt_leeway, max_retries=max_retries, ) - self._client = httpx.Client(timeout=request_timeout, follow_redirects=True) + self._client = httpx.Client( + timeout=self._request_timeout, follow_redirects=True + ) def close(self) -> None: """Close the underlying HTTP client and release resources.""" @@ -686,12 +1137,12 @@ def user_management_users(self) -> UserManagementUsersNamespace: return UserManagementUsersNamespace(self) @functools.cached_property - def connect(self) -> Any: - return self.workos_connect + def directory_sync(self) -> DirectorySyncNamespace: + return DirectorySyncNamespace(self) @functools.cached_property - def directory_sync(self) -> Any: - return self.directories + def connect(self) -> Any: + return self.workos_connect @functools.cached_property def fga(self) -> Any: @@ -703,7 +1154,9 @@ def mfa(self) -> Any: @functools.cached_property def passwordless(self) -> Any: - return object() # Backward-compatible stub + from .passwordless import Passwordless + + return Passwordless(self) @functools.cached_property def portal(self) -> Any: @@ -711,7 +1164,9 @@ def portal(self) -> Any: @functools.cached_property def vault(self) -> Any: - return object() # Backward-compatible stub + from .vault import Vault + + return Vault(self) @overload def request( @@ -844,8 +1299,9 @@ def __init__( *, api_key: Optional[str] = None, client_id: Optional[str] = None, - base_url: str = "https://api.workos.com", - request_timeout: int = 25, + base_url: Optional[str] = None, + request_timeout: Optional[int] = None, + jwt_leeway: float = 0.0, max_retries: int = MAX_RETRIES, ) -> None: """Initialize the async WorkOS client. @@ -853,8 +1309,9 @@ def __init__( Args: api_key: WorkOS API key. Falls back to the WORKOS_API_KEY environment variable. client_id: WorkOS client ID. Falls back to the WORKOS_CLIENT_ID environment variable. - base_url: Base URL for API requests. Defaults to "https://api.workos.com". - request_timeout: HTTP request timeout in seconds. Defaults to 25. + base_url: Base URL for API requests. Falls back to WORKOS_BASE_URL or "https://api.workos.com". + request_timeout: HTTP request timeout in seconds. Falls back to WORKOS_REQUEST_TIMEOUT or 25. + jwt_leeway: JWT clock skew leeway in seconds. max_retries: Maximum number of retries for failed requests. Defaults to 3. Raises: @@ -865,9 +1322,12 @@ def __init__( client_id=client_id, base_url=base_url, request_timeout=request_timeout, + jwt_leeway=jwt_leeway, max_retries=max_retries, ) - self._client = httpx.AsyncClient(timeout=request_timeout, follow_redirects=True) + self._client = httpx.AsyncClient( + timeout=self._request_timeout, follow_redirects=True + ) async def close(self) -> None: """Close the underlying HTTP client and release resources.""" @@ -976,12 +1436,12 @@ def user_management_users(self) -> AsyncUserManagementUsersNamespace: return AsyncUserManagementUsersNamespace(self) @functools.cached_property - def connect(self) -> Any: - return self.workos_connect + def directory_sync(self) -> AsyncDirectorySyncNamespace: + return AsyncDirectorySyncNamespace(self) @functools.cached_property - def directory_sync(self) -> Any: - return self.directories + def connect(self) -> Any: + return self.workos_connect @functools.cached_property def fga(self) -> Any: @@ -993,7 +1453,9 @@ def mfa(self) -> Any: @functools.cached_property def passwordless(self) -> Any: - return object() # Backward-compatible stub + from .passwordless import AsyncPasswordless + + return AsyncPasswordless(self) @functools.cached_property def portal(self) -> Any: @@ -1001,7 +1463,9 @@ def portal(self) -> Any: @functools.cached_property def vault(self) -> Any: - return object() # Backward-compatible stub + from .vault import AsyncVault + + return AsyncVault(self) @overload async def request( @@ -1124,3 +1588,8 @@ async def _fetch(*, after: Optional[str] = None) -> AsyncPage[D]: ) return AsyncPage(data=items, list_metadata=list_metadata, _fetch_page=_fetch) + + +# Backward-compatible aliases +WorkOS = WorkOSClient +AsyncWorkOS = AsyncWorkOSClient diff --git a/src/workos/_errors.py b/src/workos/_errors.py new file mode 100644 index 00000000..79da3c1b --- /dev/null +++ b/src/workos/_errors.py @@ -0,0 +1,314 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import Dict, Optional, Type + + +class BaseRequestException(Exception): + """Base exception for all WorkOS errors.""" + + message: str + status_code: Optional[int] + request_id: Optional[str] + code: Optional[str] + param: Optional[str] + raw_body: Optional[str] + request_url: Optional[str] + request_method: Optional[str] + + def __init__( + self, + message: str, + *, + status_code: Optional[int] = None, + request_id: Optional[str] = None, + code: Optional[str] = None, + param: Optional[str] = None, + raw_body: Optional[str] = None, + request_url: Optional[str] = None, + request_method: Optional[str] = None, + ) -> None: + super().__init__(message) + self.message = message + self.status_code = status_code + self.request_id = request_id + self.code = code + self.param = param + self.raw_body = raw_body + self.request_url = request_url + self.request_method = request_method + + +class BadRequestException(BaseRequestException): + """400 Bad Request.""" + + def __init__( + self, + message: str = "Bad request", + *, + request_id: Optional[str] = None, + code: Optional[str] = None, + param: Optional[str] = None, + raw_body: Optional[str] = None, + request_url: Optional[str] = None, + request_method: Optional[str] = None, + ) -> None: + super().__init__( + message, + status_code=400, + request_id=request_id, + code=code, + param=param, + raw_body=raw_body, + request_url=request_url, + request_method=request_method, + ) + + +class AuthenticationException(BaseRequestException): + """401 Unauthorized.""" + + def __init__( + self, + message: str = "Unauthorized", + *, + request_id: Optional[str] = None, + code: Optional[str] = None, + param: Optional[str] = None, + raw_body: Optional[str] = None, + request_url: Optional[str] = None, + request_method: Optional[str] = None, + ) -> None: + super().__init__( + message, + status_code=401, + request_id=request_id, + code=code, + param=param, + raw_body=raw_body, + request_url=request_url, + request_method=request_method, + ) + + +class AuthorizationException(BaseRequestException): + """403 Forbidden.""" + + def __init__( + self, + message: str = "Forbidden", + *, + request_id: Optional[str] = None, + code: Optional[str] = None, + param: Optional[str] = None, + raw_body: Optional[str] = None, + request_url: Optional[str] = None, + request_method: Optional[str] = None, + ) -> None: + super().__init__( + message, + status_code=403, + request_id=request_id, + code=code, + param=param, + raw_body=raw_body, + request_url=request_url, + request_method=request_method, + ) + + +class EmailVerificationRequiredException(AuthorizationException): + """Raised when email verification is required before authentication.""" + + email_verification_id: Optional[str] + + def __init__( + self, + message: str = "Email verification required", + *, + email_verification_id: Optional[str] = None, + request_id: Optional[str] = None, + code: Optional[str] = None, + param: Optional[str] = None, + raw_body: Optional[str] = None, + request_url: Optional[str] = None, + request_method: Optional[str] = None, + ) -> None: + super().__init__( + message, + request_id=request_id, + code=code, + param=param, + raw_body=raw_body, + request_url=request_url, + request_method=request_method, + ) + self.email_verification_id = email_verification_id + + +class NotFoundException(BaseRequestException): + """404 Not Found.""" + + def __init__( + self, + message: str = "Not found", + *, + request_id: Optional[str] = None, + code: Optional[str] = None, + param: Optional[str] = None, + raw_body: Optional[str] = None, + request_url: Optional[str] = None, + request_method: Optional[str] = None, + ) -> None: + super().__init__( + message, + status_code=404, + request_id=request_id, + code=code, + param=param, + raw_body=raw_body, + request_url=request_url, + request_method=request_method, + ) + + +class ConflictException(BaseRequestException): + """409 Conflict.""" + + def __init__( + self, + message: str = "Conflict", + *, + request_id: Optional[str] = None, + code: Optional[str] = None, + param: Optional[str] = None, + raw_body: Optional[str] = None, + request_url: Optional[str] = None, + request_method: Optional[str] = None, + ) -> None: + super().__init__( + message, + status_code=409, + request_id=request_id, + code=code, + param=param, + raw_body=raw_body, + request_url=request_url, + request_method=request_method, + ) + + +class UnprocessableEntityException(BaseRequestException): + """422 Unprocessable Entity.""" + + def __init__( + self, + message: str = "Unprocessable entity", + *, + request_id: Optional[str] = None, + code: Optional[str] = None, + param: Optional[str] = None, + raw_body: Optional[str] = None, + request_url: Optional[str] = None, + request_method: Optional[str] = None, + ) -> None: + super().__init__( + message, + status_code=422, + request_id=request_id, + code=code, + param=param, + raw_body=raw_body, + request_url=request_url, + request_method=request_method, + ) + + +class RateLimitExceededException(BaseRequestException): + """429 Rate Limited.""" + + retry_after: Optional[float] + + def __init__( + self, + message: str = "Too many requests", + *, + retry_after: Optional[float] = None, + request_id: Optional[str] = None, + code: Optional[str] = None, + param: Optional[str] = None, + raw_body: Optional[str] = None, + request_url: Optional[str] = None, + request_method: Optional[str] = None, + ) -> None: + super().__init__( + message, + status_code=429, + request_id=request_id, + code=code, + param=param, + raw_body=raw_body, + request_url=request_url, + request_method=request_method, + ) + self.retry_after = retry_after + + +class ServerException(BaseRequestException): + """500+ Server Error.""" + + def __init__( + self, + message: str = "Server error", + *, + status_code: int = 500, + request_id: Optional[str] = None, + code: Optional[str] = None, + param: Optional[str] = None, + raw_body: Optional[str] = None, + request_url: Optional[str] = None, + request_method: Optional[str] = None, + ) -> None: + super().__init__( + message, + status_code=status_code, + request_id=request_id, + code=code, + param=param, + raw_body=raw_body, + request_url=request_url, + request_method=request_method, + ) + + +class ConfigurationException(BaseRequestException): + """Missing or invalid configuration.""" + + def __init__(self, message: str = "Configuration error") -> None: + super().__init__(message) + + +class WorkOSConnectionException(BaseRequestException): + """Raised when the SDK cannot connect to the API (DNS failure, connection refused, etc.).""" + + def __init__(self, message: str = "Connection failed") -> None: + super().__init__(message) + + +class WorkOSTimeoutException(BaseRequestException): + """Raised when the API request times out.""" + + def __init__(self, message: str = "Request timed out") -> None: + super().__init__(message) + + +STATUS_CODE_TO_EXCEPTION: Dict[int, Type[BaseRequestException]] = { + 400: BadRequestException, + 401: AuthenticationException, + 403: AuthorizationException, + 404: NotFoundException, + 409: ConflictException, + 422: UnprocessableEntityException, + 429: RateLimitExceededException, +} diff --git a/src/workos/_pagination.py b/src/workos/_pagination.py new file mode 100644 index 00000000..7839eddf --- /dev/null +++ b/src/workos/_pagination.py @@ -0,0 +1,107 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import ( + Any, + AsyncIterator, + Awaitable, + Callable, + Dict, + Generic, + Iterator, + List, + Optional, + TypeVar, +) + +from ._types import Deserializable + +T = TypeVar("T", bound=Deserializable) + + +@dataclass +class SyncPage(Generic[T]): + """A page of results with auto-pagination support.""" + + data: List[T] + list_metadata: Dict[str, Any] + _fetch_page: Optional[Callable[..., "SyncPage[T]"]] = field( + default=None, repr=False + ) + + @property + def before(self) -> Optional[str]: + """Cursor for the previous page, if available.""" + return self.list_metadata.get("before") + + @property + def after(self) -> Optional[str]: + """Cursor for the next page, if available.""" + return self.list_metadata.get("after") + + def has_more(self) -> bool: + """Whether there are more pages available.""" + return self.after is not None + + def auto_paging_iter(self) -> Iterator[T]: + """Iterate through all items across all pages.""" + page = self + while True: + yield from page.data + if not page.data: + break + if not page.has_more() or page._fetch_page is None: + break + page = page._fetch_page(after=page.after) + + def __iter__(self) -> Iterator[T]: + """Iterate through all items across all pages.""" + return self.auto_paging_iter() + + +@dataclass +class AsyncPage(Generic[T]): + """A page of results with async auto-pagination support.""" + + data: List[T] + list_metadata: Dict[str, Any] + _fetch_page: Optional[Callable[..., Awaitable["AsyncPage[T]"]]] = field( + default=None, repr=False + ) + + @property + def before(self) -> Optional[str]: + """Cursor for the previous page, if available.""" + return self.list_metadata.get("before") + + @property + def after(self) -> Optional[str]: + """Cursor for the next page, if available.""" + return self.list_metadata.get("after") + + def has_more(self) -> bool: + """Whether there are more pages available.""" + return self.after is not None + + async def auto_paging_iter(self) -> AsyncIterator[T]: + """Iterate through all items across all pages.""" + page = self + while True: + for item in page.data: + yield item + if not page.data: + break + if not page.has_more() or page._fetch_page is None: + break + page = await page._fetch_page(after=page.after) + + async def __aiter__(self) -> AsyncIterator[T]: + """Iterate through all items across all pages.""" + async for item in self.auto_paging_iter(): + yield item + + +# Backward-compatible alias (v5.x naming) +WorkOSListResource = SyncPage diff --git a/src/workos/_types.py b/src/workos/_types.py new file mode 100644 index 00000000..499d0e52 --- /dev/null +++ b/src/workos/_types.py @@ -0,0 +1,26 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import Any, Dict, Protocol, TypeVar +from typing_extensions import Self, TypedDict + + +class RequestOptions(TypedDict, total=False): + """Per-request options that can be passed to any API method.""" + + extra_headers: Dict[str, str] + timeout: float + idempotency_key: str + max_retries: int + base_url: str + + +class Deserializable(Protocol): + """Protocol for types that can be deserialized from a dict.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> Self: ... + + +D = TypeVar("D", bound=Deserializable) diff --git a/src/workos/admin_portal/__init__.py b/src/workos/admin_portal/__init__.py new file mode 100644 index 00000000..10d97260 --- /dev/null +++ b/src/workos/admin_portal/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import AdminPortal, AsyncAdminPortal +from .models import * diff --git a/src/workos/admin_portal/_resource.py b/src/workos/admin_portal/_resource.py new file mode 100644 index 00000000..0082d2f7 --- /dev/null +++ b/src/workos/admin_portal/_resource.py @@ -0,0 +1,152 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Optional + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import IntentOptions, PortalLinkResponse +from workos.common.models import GenerateLinkDtoIntent +from .._types import RequestOptions + + +class AdminPortal: + """Admin Portal API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def create( + self, + *, + organization: str, + return_url: Optional[str] = None, + success_url: Optional[str] = None, + intent: Optional[GenerateLinkDtoIntent] = None, + intent_options: Optional[IntentOptions] = None, + request_options: Optional[RequestOptions] = None, + ) -> PortalLinkResponse: + """Generate a Portal Link + + Generate a Portal Link scoped to an Organization. + + Args: + return_url: The URL to go to when an admin clicks on your logo in the Admin Portal. If not specified, the return URL configured on the [Redirects](https://dashboard.workos.com/redirects) page will be used. + success_url: The URL to redirect the admin to when they finish setup. If not specified, the success URL configured on the [Redirects](https://dashboard.workos.com/redirects) page will be used. + organization: An [Organization](https://workos.com/docs/reference/organization) identifier. + intent: + The intent of the Admin Portal. + - `sso` - Launch Admin Portal for creating SSO connections + - `dsync` - Launch Admin Portal for creating Directory Sync connections + - `audit_logs` - Launch Admin Portal for viewing Audit Logs + - `log_streams` - Launch Admin Portal for creating Log Streams + - `domain_verification` - Launch Admin Portal for Domain Verification + - `certificate_renewal` - Launch Admin Portal for renewing SAML Certificates + - `bring_your_own_key` - Launch Admin Portal for configuring Bring Your Own Key + intent_options: Options to configure the Admin Portal based on the intent. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + PortalLinkResponse + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "return_url": return_url, + "success_url": success_url, + "organization": organization, + "intent": intent, + "intent_options": intent_options.to_dict() + if intent_options is not None + else None, + }.items() + if v is not None + } + return self._client.request( + method="post", + path="portal/generate_link", + body=body, + model=PortalLinkResponse, + request_options=request_options, + ) + + +class AsyncAdminPortal: + """Admin Portal API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def create( + self, + *, + organization: str, + return_url: Optional[str] = None, + success_url: Optional[str] = None, + intent: Optional[GenerateLinkDtoIntent] = None, + intent_options: Optional[IntentOptions] = None, + request_options: Optional[RequestOptions] = None, + ) -> PortalLinkResponse: + """Generate a Portal Link + + Generate a Portal Link scoped to an Organization. + + Args: + return_url: The URL to go to when an admin clicks on your logo in the Admin Portal. If not specified, the return URL configured on the [Redirects](https://dashboard.workos.com/redirects) page will be used. + success_url: The URL to redirect the admin to when they finish setup. If not specified, the success URL configured on the [Redirects](https://dashboard.workos.com/redirects) page will be used. + organization: An [Organization](https://workos.com/docs/reference/organization) identifier. + intent: + The intent of the Admin Portal. + - `sso` - Launch Admin Portal for creating SSO connections + - `dsync` - Launch Admin Portal for creating Directory Sync connections + - `audit_logs` - Launch Admin Portal for viewing Audit Logs + - `log_streams` - Launch Admin Portal for creating Log Streams + - `domain_verification` - Launch Admin Portal for Domain Verification + - `certificate_renewal` - Launch Admin Portal for renewing SAML Certificates + - `bring_your_own_key` - Launch Admin Portal for configuring Bring Your Own Key + intent_options: Options to configure the Admin Portal based on the intent. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + PortalLinkResponse + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "return_url": return_url, + "success_url": success_url, + "organization": organization, + "intent": intent, + "intent_options": intent_options.to_dict() + if intent_options is not None + else None, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path="portal/generate_link", + body=body, + model=PortalLinkResponse, + request_options=request_options, + ) diff --git a/src/workos/admin_portal/models/__init__.py b/src/workos/admin_portal/models/__init__.py new file mode 100644 index 00000000..1eaede20 --- /dev/null +++ b/src/workos/admin_portal/models/__init__.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .generate_link import GenerateLink as GenerateLink +from .intent_options import IntentOptions as IntentOptions +from .portal_link_response import PortalLinkResponse as PortalLinkResponse +from .sso_intent_options import SSOIntentOptions as SSOIntentOptions diff --git a/src/workos/admin_portal/models/generate_link.py b/src/workos/admin_portal/models/generate_link.py new file mode 100644 index 00000000..3cc40b9f --- /dev/null +++ b/src/workos/admin_portal/models/generate_link.py @@ -0,0 +1,69 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + +from .intent_options import IntentOptions +from workos.common.models import GenerateLinkDtoIntent + + +@dataclass(slots=True) +class GenerateLink: + """Generate Link model.""" + + organization: str + """An [Organization](https://workos.com/docs/reference/organization) identifier.""" + return_url: Optional[str] = None + """The URL to go to when an admin clicks on your logo in the Admin Portal. If not specified, the return URL configured on the [Redirects](https://dashboard.workos.com/redirects) page will be used.""" + success_url: Optional[str] = None + """The URL to redirect the admin to when they finish setup. If not specified, the success URL configured on the [Redirects](https://dashboard.workos.com/redirects) page will be used.""" + intent: Optional["GenerateLinkDtoIntent"] = None + """ + The intent of the Admin Portal. + - `sso` - Launch Admin Portal for creating SSO connections + - `dsync` - Launch Admin Portal for creating Directory Sync connections + - `audit_logs` - Launch Admin Portal for viewing Audit Logs + - `log_streams` - Launch Admin Portal for creating Log Streams + - `domain_verification` - Launch Admin Portal for Domain Verification + - `certificate_renewal` - Launch Admin Portal for renewing SAML Certificates + - `bring_your_own_key` - Launch Admin Portal for configuring Bring Your Own Key""" + intent_options: Optional["IntentOptions"] = None + """Options to configure the Admin Portal based on the intent.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "GenerateLink": + """Deserialize from a dictionary.""" + try: + return cls( + organization=data["organization"], + return_url=data.get("return_url"), + success_url=data.get("success_url"), + intent=GenerateLinkDtoIntent(_v) + if (_v := data.get("intent")) is not None + else None, + intent_options=IntentOptions.from_dict(cast(Dict[str, Any], _v)) + if (_v := data.get("intent_options")) is not None + else None, + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing GenerateLink: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["organization"] = self.organization + if self.return_url is not None: + result["return_url"] = self.return_url + if self.success_url is not None: + result["success_url"] = self.success_url + if self.intent is not None: + result["intent"] = self.intent + if self.intent_options is not None: + result["intent_options"] = self.intent_options.to_dict() + return result diff --git a/src/workos/admin_portal/models/intent_options.py b/src/workos/admin_portal/models/intent_options.py new file mode 100644 index 00000000..abf0c3d8 --- /dev/null +++ b/src/workos/admin_portal/models/intent_options.py @@ -0,0 +1,36 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict +from workos._errors import BaseRequestException + +from .sso_intent_options import SSOIntentOptions + + +@dataclass(slots=True) +class IntentOptions: + """Intent Options model.""" + + sso: "SSOIntentOptions" + """SSO-specific options for the Admin Portal.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "IntentOptions": + """Deserialize from a dictionary.""" + try: + return cls( + sso=SSOIntentOptions.from_dict(cast(Dict[str, Any], data["sso"])), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing IntentOptions: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["sso"] = self.sso.to_dict() + return result diff --git a/src/workos/admin_portal/models/portal_link_response.py b/src/workos/admin_portal/models/portal_link_response.py new file mode 100644 index 00000000..567f2120 --- /dev/null +++ b/src/workos/admin_portal/models/portal_link_response.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class PortalLinkResponse: + """Portal Link Response model.""" + + link: str + """An ephemeral link to initiate the Admin Portal.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "PortalLinkResponse": + """Deserialize from a dictionary.""" + try: + return cls( + link=data["link"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing PortalLinkResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["link"] = self.link + return result diff --git a/src/workos/admin_portal/models/sso_intent_options.py b/src/workos/admin_portal/models/sso_intent_options.py new file mode 100644 index 00000000..15f027db --- /dev/null +++ b/src/workos/admin_portal/models/sso_intent_options.py @@ -0,0 +1,39 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class SSOIntentOptions: + """SSO Intent Options model.""" + + bookmark_slug: Optional[str] = None + """The bookmark slug to use for SSO.""" + provider_type: Optional[Literal["GoogleSAML"]] = None + """The SSO provider type to configure.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "SSOIntentOptions": + """Deserialize from a dictionary.""" + try: + return cls( + bookmark_slug=data.get("bookmark_slug"), + provider_type=data.get("provider_type"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing SSOIntentOptions: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + if self.bookmark_slug is not None: + result["bookmark_slug"] = self.bookmark_slug + if self.provider_type is not None: + result["provider_type"] = self.provider_type + return result diff --git a/src/workos/api_keys/__init__.py b/src/workos/api_keys/__init__.py new file mode 100644 index 00000000..a59627dc --- /dev/null +++ b/src/workos/api_keys/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import ApiKeys, AsyncApiKeys +from .models import * diff --git a/src/workos/api_keys/_resource.py b/src/workos/api_keys/_resource.py new file mode 100644 index 00000000..c0b71176 --- /dev/null +++ b/src/workos/api_keys/_resource.py @@ -0,0 +1,149 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Optional + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import ApiKeyValidationResponse +from .._types import RequestOptions + + +class ApiKeys: + """Api Keys API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def validate_api_key( + self, + *, + value: str, + request_options: Optional[RequestOptions] = None, + ) -> ApiKeyValidationResponse: + """Validate API key + + Validate an API key value and return the API key object if valid. + + Args: + value: The value for an API key. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + ApiKeyValidationResponse + + Raises: + AuthenticationException: If the API key is invalid (401). + UnprocessableEntityException: If the request data is unprocessable (422). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "value": value, + } + return self._client.request( + method="post", + path="api_keys/validations", + body=body, + model=ApiKeyValidationResponse, + request_options=request_options, + ) + + def delete_api_key( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete an API key + + Permanently deletes an API key. This action cannot be undone. Once deleted, any requests using this API key will fail authentication. + + Args: + id: The unique ID of the API key. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + self._client.request( + method="delete", + path=f"api_keys/{id}", + request_options=request_options, + ) + + delete = delete_api_key + + +class AsyncApiKeys: + """Api Keys API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def validate_api_key( + self, + *, + value: str, + request_options: Optional[RequestOptions] = None, + ) -> ApiKeyValidationResponse: + """Validate API key + + Validate an API key value and return the API key object if valid. + + Args: + value: The value for an API key. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + ApiKeyValidationResponse + + Raises: + AuthenticationException: If the API key is invalid (401). + UnprocessableEntityException: If the request data is unprocessable (422). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "value": value, + } + return await self._client.request( + method="post", + path="api_keys/validations", + body=body, + model=ApiKeyValidationResponse, + request_options=request_options, + ) + + async def delete_api_key( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete an API key + + Permanently deletes an API key. This action cannot be undone. Once deleted, any requests using this API key will fail authentication. + + Args: + id: The unique ID of the API key. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + await self._client.request( + method="delete", + path=f"api_keys/{id}", + request_options=request_options, + ) + + delete = delete_api_key diff --git a/src/workos/api_keys/models/__init__.py b/src/workos/api_keys/models/__init__.py new file mode 100644 index 00000000..a3789df5 --- /dev/null +++ b/src/workos/api_keys/models/__init__.py @@ -0,0 +1,8 @@ +# This file is auto-generated by oagen. Do not edit. + +from .api_key import ApiKey as ApiKey +from .api_key_owner import ApiKeyOwner as ApiKeyOwner +from .api_key_validation_response import ( + ApiKeyValidationResponse as ApiKeyValidationResponse, +) +from .validate_api_key import ValidateApiKey as ValidateApiKey diff --git a/src/workos/api_keys/models/api_key.py b/src/workos/api_keys/models/api_key.py new file mode 100644 index 00000000..4be75219 --- /dev/null +++ b/src/workos/api_keys/models/api_key.py @@ -0,0 +1,80 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import cast +from typing import Any, Dict, List, Literal, Optional +from workos._errors import BaseRequestException + +from .api_key_owner import ApiKeyOwner + + +@dataclass(slots=True) +class ApiKey: + """The API Key object if the value is valid, or `null` if invalid.""" + + object: Literal["api_key"] + """Distinguishes the API Key object.""" + id: str + """Unique identifier of the API Key.""" + owner: "ApiKeyOwner" + """The entity that owns the API Key.""" + name: str + """A descriptive name for the API Key.""" + obfuscated_value: str + """An obfuscated representation of the API Key value.""" + last_used_at: Optional[str] + """Timestamp of when the API Key was last used.""" + permissions: List[str] + """The permission slugs assigned to the API Key.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ApiKey": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + owner=ApiKeyOwner.from_dict(cast(Dict[str, Any], data["owner"])), + name=data["name"], + obfuscated_value=data["obfuscated_value"], + last_used_at=data["last_used_at"], + permissions=data["permissions"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing ApiKey: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["owner"] = self.owner.to_dict() + result["name"] = self.name + result["obfuscated_value"] = self.obfuscated_value + if self.last_used_at is not None: + result["last_used_at"] = self.last_used_at + else: + result["last_used_at"] = None + result["permissions"] = self.permissions + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + return result diff --git a/src/workos/api_keys/models/api_key_owner.py b/src/workos/api_keys/models/api_key_owner.py new file mode 100644 index 00000000..b839cb3c --- /dev/null +++ b/src/workos/api_keys/models/api_key_owner.py @@ -0,0 +1,37 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Literal +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class ApiKeyOwner: + """The entity that owns the API Key.""" + + type: Literal["organization"] + """The type of the API Key owner.""" + id: str + """Unique identifier of the API Key owner.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ApiKeyOwner": + """Deserialize from a dictionary.""" + try: + return cls( + type=data["type"], + id=data["id"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing ApiKeyOwner: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["type"] = self.type + result["id"] = self.id + return result diff --git a/src/workos/api_keys/models/api_key_validation_response.py b/src/workos/api_keys/models/api_key_validation_response.py new file mode 100644 index 00000000..d2d9c209 --- /dev/null +++ b/src/workos/api_keys/models/api_key_validation_response.py @@ -0,0 +1,40 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + +from .api_key import ApiKey + + +@dataclass(slots=True) +class ApiKeyValidationResponse: + """Api Key Validation Response model.""" + + api_key: Optional["ApiKey"] + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ApiKeyValidationResponse": + """Deserialize from a dictionary.""" + try: + return cls( + api_key=ApiKey.from_dict(cast(Dict[str, Any], _v)) + if (_v := data["api_key"]) is not None + else None, + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing ApiKeyValidationResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + if self.api_key is not None: + result["api_key"] = self.api_key.to_dict() + else: + result["api_key"] = None + return result diff --git a/src/workos/api_keys/models/validate_api_key.py b/src/workos/api_keys/models/validate_api_key.py new file mode 100644 index 00000000..da0ca498 --- /dev/null +++ b/src/workos/api_keys/models/validate_api_key.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class ValidateApiKey: + """Validate Api Key model.""" + + value: str + """The value for an API key.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ValidateApiKey": + """Deserialize from a dictionary.""" + try: + return cls( + value=data["value"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing ValidateApiKey: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["value"] = self.value + return result diff --git a/src/workos/application_client_secrets/__init__.py b/src/workos/application_client_secrets/__init__.py new file mode 100644 index 00000000..0265a361 --- /dev/null +++ b/src/workos/application_client_secrets/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import ApplicationClientSecrets, AsyncApplicationClientSecrets +from .models import * diff --git a/src/workos/application_client_secrets/_resource.py b/src/workos/application_client_secrets/_resource.py new file mode 100644 index 00000000..0f981d56 --- /dev/null +++ b/src/workos/application_client_secrets/_resource.py @@ -0,0 +1,209 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import ApplicationCredentialsListItem, NewConnectApplicationSecret +from .._types import RequestOptions + + +class ApplicationClientSecrets: + """Application Client Secrets API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def list( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> List[ApplicationCredentialsListItem]: + """List Client Secrets for a Connect Application + + List all client secrets associated with a Connect Application. + + Args: + id: The application ID or client ID of the Connect Application. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + List[ApplicationCredentialsListItem] + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + raw = self._client.request( + method="get", + path=f"connect/applications/{id}/client_secrets", + request_options=request_options, + ) + return [ + ApplicationCredentialsListItem.from_dict(cast(Dict[str, Any], item)) + for item in (raw if isinstance(raw, list) else []) + ] + + def create( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> NewConnectApplicationSecret: + """Create a new client secret for a Connect Application + + Create new secrets for a Connect Application. + + Args: + id: The application ID or client ID of the Connect Application. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + NewConnectApplicationSecret + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = {} + return self._client.request( + method="post", + path=f"connect/applications/{id}/client_secrets", + body=body, + model=NewConnectApplicationSecret, + request_options=request_options, + ) + + def delete( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a Client Secret + + Delete (revoke) an existing client secret. + + Args: + id: The unique ID of the client secret. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + self._client.request( + method="delete", + path=f"connect/client_secrets/{id}", + request_options=request_options, + ) + + +class AsyncApplicationClientSecrets: + """Application Client Secrets API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def list( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> List[ApplicationCredentialsListItem]: + """List Client Secrets for a Connect Application + + List all client secrets associated with a Connect Application. + + Args: + id: The application ID or client ID of the Connect Application. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + List[ApplicationCredentialsListItem] + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + raw = await self._client.request( + method="get", + path=f"connect/applications/{id}/client_secrets", + request_options=request_options, + ) + return [ + ApplicationCredentialsListItem.from_dict(cast(Dict[str, Any], item)) + for item in (raw if isinstance(raw, list) else []) + ] + + async def create( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> NewConnectApplicationSecret: + """Create a new client secret for a Connect Application + + Create new secrets for a Connect Application. + + Args: + id: The application ID or client ID of the Connect Application. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + NewConnectApplicationSecret + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = {} + return await self._client.request( + method="post", + path=f"connect/applications/{id}/client_secrets", + body=body, + model=NewConnectApplicationSecret, + request_options=request_options, + ) + + async def delete( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a Client Secret + + Delete (revoke) an existing client secret. + + Args: + id: The unique ID of the client secret. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + await self._client.request( + method="delete", + path=f"connect/client_secrets/{id}", + request_options=request_options, + ) diff --git a/src/workos/application_client_secrets/models/__init__.py b/src/workos/application_client_secrets/models/__init__.py new file mode 100644 index 00000000..8c0a6e3c --- /dev/null +++ b/src/workos/application_client_secrets/models/__init__.py @@ -0,0 +1,11 @@ +# This file is auto-generated by oagen. Do not edit. + +from .application_credentials_list_item import ( + ApplicationCredentialsListItem as ApplicationCredentialsListItem, +) +from .create_application_secret import ( + CreateApplicationSecret as CreateApplicationSecret, +) +from .new_connect_application_secret import ( + NewConnectApplicationSecret as NewConnectApplicationSecret, +) diff --git a/src/workos/application_client_secrets/models/application_credentials_list_item.py b/src/workos/application_client_secrets/models/application_credentials_list_item.py new file mode 100644 index 00000000..e115774d --- /dev/null +++ b/src/workos/application_client_secrets/models/application_credentials_list_item.py @@ -0,0 +1,65 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class ApplicationCredentialsListItem: + """Application Credentials List Item model.""" + + object: Literal["connect_application_secret"] + """Distinguishes the connect application secret object.""" + id: str + """The unique ID of the client secret.""" + secret_hint: str + """A hint showing the last few characters of the secret value.""" + last_used_at: Optional[str] + """The timestamp when the client secret was last used, or null if never used.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ApplicationCredentialsListItem": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + secret_hint=data["secret_hint"], + last_used_at=data["last_used_at"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing ApplicationCredentialsListItem: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["secret_hint"] = self.secret_hint + if self.last_used_at is not None: + result["last_used_at"] = self.last_used_at + else: + result["last_used_at"] = None + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + return result diff --git a/src/workos/application_client_secrets/models/create_application_secret.py b/src/workos/application_client_secrets/models/create_application_secret.py new file mode 100644 index 00000000..31f51833 --- /dev/null +++ b/src/workos/application_client_secrets/models/create_application_secret.py @@ -0,0 +1,29 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class CreateApplicationSecret: + """Create Application Secret model.""" + + pass + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CreateApplicationSecret": + """Deserialize from a dictionary.""" + try: + return cls() + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing CreateApplicationSecret: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + return result diff --git a/src/workos/application_client_secrets/models/new_connect_application_secret.py b/src/workos/application_client_secrets/models/new_connect_application_secret.py new file mode 100644 index 00000000..e9974cb2 --- /dev/null +++ b/src/workos/application_client_secrets/models/new_connect_application_secret.py @@ -0,0 +1,69 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class NewConnectApplicationSecret: + """New Connect Application Secret model.""" + + object: Literal["connect_application_secret"] + """Distinguishes the connect application secret object.""" + id: str + """The unique ID of the client secret.""" + secret_hint: str + """A hint showing the last few characters of the secret value.""" + last_used_at: Optional[str] + """The timestamp when the client secret was last used, or null if never used.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + secret: str + """The plaintext secret value. Only returned at creation time and cannot be retrieved later.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "NewConnectApplicationSecret": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + secret_hint=data["secret_hint"], + last_used_at=data["last_used_at"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + secret=data["secret"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing NewConnectApplicationSecret: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["secret_hint"] = self.secret_hint + if self.last_used_at is not None: + result["last_used_at"] = self.last_used_at + else: + result["last_used_at"] = None + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["secret"] = self.secret + return result diff --git a/src/workos/applications/__init__.py b/src/workos/applications/__init__.py new file mode 100644 index 00000000..1e1c5fa8 --- /dev/null +++ b/src/workos/applications/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import Applications, AsyncApplications +from .models import * diff --git a/src/workos/applications/_resource.py b/src/workos/applications/_resource.py new file mode 100644 index 00000000..b205af7c --- /dev/null +++ b/src/workos/applications/_resource.py @@ -0,0 +1,418 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import ( + ConnectApplication, + CreateM2MApplication, + CreateOAuthApplication, + RedirectUriDto, +) +from .models import ApplicationsOrder +from .._pagination import AsyncPage, SyncPage +from .._types import RequestOptions + + +class Applications: + """Applications API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[ApplicationsOrder] = None, + organization_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[ConnectApplication]: + """List Connect Applications + + List all Connect Applications in the current environment with optional filtering. + + Args: + organization_id: Filter Connect Applications by organization ID. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[ConnectApplication] + + Raises: + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "organization_id": organization_id, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path="connect/applications", + model=ConnectApplication, + params=params, + request_options=request_options, + ) + + def create( + self, + *, + body: Union[CreateOAuthApplication, CreateM2MApplication, Dict[str, Any]], + request_options: Optional[RequestOptions] = None, + ) -> ConnectApplication: + """Create a Connect Application + + Create a new Connect Application. Supports both OAuth and Machine-to-Machine (M2M) application types. + + Args: + body: The request body. Accepts: CreateOAuthApplication, CreateM2MApplication, or a plain dict. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + ConnectApplication + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + _body: Dict[str, Any] = body if isinstance(body, dict) else body.to_dict() + return self._client.request( + method="post", + path="connect/applications", + body=_body, + model=ConnectApplication, + request_options=request_options, + ) + + def get( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> ConnectApplication: + """Get a Connect Application + + Retrieve details for a specific Connect Application by ID or client ID. + + Args: + id: The application ID or client ID of the Connect Application. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + ConnectApplication + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"connect/applications/{id}", + model=ConnectApplication, + request_options=request_options, + ) + + find = get + + def update( + self, + id: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + scopes: Optional[List[str]] = None, + redirect_uris: Optional[List[RedirectUriDto]] = None, + request_options: Optional[RequestOptions] = None, + ) -> ConnectApplication: + """Update a Connect Application + + Update an existing Connect Application. For OAuth applications, you can update redirect URIs. For all applications, you can update the name, description, and scopes. + + Args: + id: The application ID or client ID of the Connect Application. + name: The name of the application. + description: A description for the application. + scopes: The OAuth scopes granted to the application. + redirect_uris: Updated redirect URIs for the application. OAuth applications only. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + ConnectApplication + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "name": name, + "description": description, + "scopes": scopes, + "redirect_uris": [item.to_dict() for item in redirect_uris] + if redirect_uris is not None + else None, + }.items() + if v is not None + } + return self._client.request( + method="put", + path=f"connect/applications/{id}", + body=body, + model=ConnectApplication, + request_options=request_options, + ) + + def delete( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a Connect Application + + Delete an existing Connect Application. + + Args: + id: The application ID or client ID of the Connect Application. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + self._client.request( + method="delete", + path=f"connect/applications/{id}", + request_options=request_options, + ) + + +class AsyncApplications: + """Applications API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[ApplicationsOrder] = None, + organization_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[ConnectApplication]: + """List Connect Applications + + List all Connect Applications in the current environment with optional filtering. + + Args: + organization_id: Filter Connect Applications by organization ID. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[ConnectApplication] + + Raises: + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "organization_id": organization_id, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path="connect/applications", + model=ConnectApplication, + params=params, + request_options=request_options, + ) + + async def create( + self, + *, + body: Union[CreateOAuthApplication, CreateM2MApplication, Dict[str, Any]], + request_options: Optional[RequestOptions] = None, + ) -> ConnectApplication: + """Create a Connect Application + + Create a new Connect Application. Supports both OAuth and Machine-to-Machine (M2M) application types. + + Args: + body: The request body. Accepts: CreateOAuthApplication, CreateM2MApplication, or a plain dict. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + ConnectApplication + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + _body: Dict[str, Any] = body if isinstance(body, dict) else body.to_dict() + return await self._client.request( + method="post", + path="connect/applications", + body=_body, + model=ConnectApplication, + request_options=request_options, + ) + + async def get( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> ConnectApplication: + """Get a Connect Application + + Retrieve details for a specific Connect Application by ID or client ID. + + Args: + id: The application ID or client ID of the Connect Application. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + ConnectApplication + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"connect/applications/{id}", + model=ConnectApplication, + request_options=request_options, + ) + + find = get + + async def update( + self, + id: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + scopes: Optional[List[str]] = None, + redirect_uris: Optional[List[RedirectUriDto]] = None, + request_options: Optional[RequestOptions] = None, + ) -> ConnectApplication: + """Update a Connect Application + + Update an existing Connect Application. For OAuth applications, you can update redirect URIs. For all applications, you can update the name, description, and scopes. + + Args: + id: The application ID or client ID of the Connect Application. + name: The name of the application. + description: A description for the application. + scopes: The OAuth scopes granted to the application. + redirect_uris: Updated redirect URIs for the application. OAuth applications only. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + ConnectApplication + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "name": name, + "description": description, + "scopes": scopes, + "redirect_uris": [item.to_dict() for item in redirect_uris] + if redirect_uris is not None + else None, + }.items() + if v is not None + } + return await self._client.request( + method="put", + path=f"connect/applications/{id}", + body=body, + model=ConnectApplication, + request_options=request_options, + ) + + async def delete( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a Connect Application + + Delete an existing Connect Application. + + Args: + id: The application ID or client ID of the Connect Application. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + await self._client.request( + method="delete", + path=f"connect/applications/{id}", + request_options=request_options, + ) diff --git a/src/workos/applications/models/__init__.py b/src/workos/applications/models/__init__.py new file mode 100644 index 00000000..acaac45e --- /dev/null +++ b/src/workos/applications/models/__init__.py @@ -0,0 +1,8 @@ +# This file is auto-generated by oagen. Do not edit. + +from .applications_order import ApplicationsOrder as ApplicationsOrder +from .connect_application import ConnectApplication as ConnectApplication +from .create_m2m_application import CreateM2MApplication as CreateM2MApplication +from .create_oauth_application import CreateOAuthApplication as CreateOAuthApplication +from .redirect_uri_dto import RedirectUriDto as RedirectUriDto +from .update_oauth_application import UpdateOAuthApplication as UpdateOAuthApplication diff --git a/src/workos/applications/models/applications_order.py b/src/workos/applications/models/applications_order.py new file mode 100644 index 00000000..8dfb1454 --- /dev/null +++ b/src/workos/applications/models/applications_order.py @@ -0,0 +1,29 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of applications order values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class ApplicationsOrder(str, Enum): + """Known values for ApplicationsOrder.""" + + NORMAL = "normal" + DESC = "desc" + ASC = "asc" + + @classmethod + def _missing_(cls, value: object) -> Optional["ApplicationsOrder"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +ApplicationsOrderLiteral: TypeAlias = Literal["normal", "desc", "asc"] diff --git a/src/workos/applications/models/connect_application.py b/src/workos/applications/models/connect_application.py new file mode 100644 index 00000000..0c1700e6 --- /dev/null +++ b/src/workos/applications/models/connect_application.py @@ -0,0 +1,73 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, List, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class ConnectApplication: + """Connect Application model.""" + + object: Literal["connect_application"] + """Distinguishes the connect application object.""" + id: str + """The unique ID of the connect application.""" + client_id: str + """The client ID of the connect application.""" + description: Optional[str] + """A description of the connect application.""" + name: str + """The name of the connect application.""" + scopes: List[str] + """The scopes available for this application.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ConnectApplication": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + client_id=data["client_id"], + description=data["description"], + name=data["name"], + scopes=data["scopes"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing ConnectApplication: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["client_id"] = self.client_id + if self.description is not None: + result["description"] = self.description + else: + result["description"] = None + result["name"] = self.name + result["scopes"] = self.scopes + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + return result diff --git a/src/workos/applications/models/create_m2m_application.py b/src/workos/applications/models/create_m2m_application.py new file mode 100644 index 00000000..117ad7ef --- /dev/null +++ b/src/workos/applications/models/create_m2m_application.py @@ -0,0 +1,55 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, List, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class CreateM2MApplication: + """Create M2M Application model.""" + + name: str + """The name of the application.""" + application_type: Literal["m2m"] + """The type of application to create.""" + organization_id: str + """The organization ID this application belongs to.""" + description: Optional[str] = None + """A description for the application.""" + scopes: Optional[List[str]] = None + """The OAuth scopes granted to the application.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CreateM2MApplication": + """Deserialize from a dictionary.""" + try: + return cls( + name=data["name"], + application_type=data["application_type"], + organization_id=data["organization_id"], + description=data.get("description"), + scopes=data.get("scopes"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing CreateM2MApplication: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["name"] = self.name + result["application_type"] = self.application_type + result["organization_id"] = self.organization_id + if self.description is not None: + result["description"] = self.description + else: + result["description"] = None + if self.scopes is not None: + result["scopes"] = self.scopes + else: + result["scopes"] = None + return result diff --git a/src/workos/applications/models/create_oauth_application.py b/src/workos/applications/models/create_oauth_application.py new file mode 100644 index 00000000..8079147a --- /dev/null +++ b/src/workos/applications/models/create_oauth_application.py @@ -0,0 +1,84 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, List, Literal, Optional +from workos._errors import BaseRequestException + +from .redirect_uri_dto import RedirectUriDto + + +@dataclass(slots=True) +class CreateOAuthApplication: + """Create O Auth Application model.""" + + name: str + """The name of the application.""" + application_type: Literal["oauth"] + """The type of application to create.""" + is_first_party: bool + """Whether this is a first-party application. Third-party applications require an organization_id.""" + description: Optional[str] = None + """A description for the application.""" + scopes: Optional[List[str]] = None + """The OAuth scopes granted to the application.""" + redirect_uris: Optional[List["RedirectUriDto"]] = None + """Redirect URIs for the application.""" + uses_pkce: Optional[bool] = None + """Whether the application uses PKCE (Proof Key for Code Exchange).""" + organization_id: Optional[str] = None + """The organization ID this application belongs to. Required when is_first_party is false.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CreateOAuthApplication": + """Deserialize from a dictionary.""" + try: + return cls( + name=data["name"], + application_type=data["application_type"], + is_first_party=data["is_first_party"], + description=data.get("description"), + scopes=data.get("scopes"), + redirect_uris=[ + RedirectUriDto.from_dict(cast(Dict[str, Any], item)) + for item in cast(list[Any], _v) + ] + if (_v := data.get("redirect_uris")) is not None + else None, + uses_pkce=data.get("uses_pkce"), + organization_id=data.get("organization_id"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing CreateOAuthApplication: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["name"] = self.name + result["application_type"] = self.application_type + result["is_first_party"] = self.is_first_party + if self.description is not None: + result["description"] = self.description + else: + result["description"] = None + if self.scopes is not None: + result["scopes"] = self.scopes + else: + result["scopes"] = None + if self.redirect_uris is not None: + result["redirect_uris"] = [item.to_dict() for item in self.redirect_uris] + else: + result["redirect_uris"] = None + if self.uses_pkce is not None: + result["uses_pkce"] = self.uses_pkce + else: + result["uses_pkce"] = None + if self.organization_id is not None: + result["organization_id"] = self.organization_id + else: + result["organization_id"] = None + return result diff --git a/src/workos/applications/models/redirect_uri_dto.py b/src/workos/applications/models/redirect_uri_dto.py new file mode 100644 index 00000000..951d44bf --- /dev/null +++ b/src/workos/applications/models/redirect_uri_dto.py @@ -0,0 +1,40 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class RedirectUriDto: + """Redirect Uri Dto model.""" + + uri: str + """The redirect URI.""" + default: Optional[bool] = None + """Whether this is the default redirect URI.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "RedirectUriDto": + """Deserialize from a dictionary.""" + try: + return cls( + uri=data["uri"], + default=data.get("default"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing RedirectUriDto: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["uri"] = self.uri + if self.default is not None: + result["default"] = self.default + else: + result["default"] = None + return result diff --git a/src/workos/applications/models/update_oauth_application.py b/src/workos/applications/models/update_oauth_application.py new file mode 100644 index 00000000..1507dd91 --- /dev/null +++ b/src/workos/applications/models/update_oauth_application.py @@ -0,0 +1,63 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, List, Optional +from workos._errors import BaseRequestException + +from .redirect_uri_dto import RedirectUriDto + + +@dataclass(slots=True) +class UpdateOAuthApplication: + """Update O Auth Application model.""" + + name: Optional[str] = None + """The name of the application.""" + description: Optional[str] = None + """A description for the application.""" + scopes: Optional[List[str]] = None + """The OAuth scopes granted to the application.""" + redirect_uris: Optional[List["RedirectUriDto"]] = None + """Updated redirect URIs for the application. OAuth applications only.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "UpdateOAuthApplication": + """Deserialize from a dictionary.""" + try: + return cls( + name=data.get("name"), + description=data.get("description"), + scopes=data.get("scopes"), + redirect_uris=[ + RedirectUriDto.from_dict(cast(Dict[str, Any], item)) + for item in cast(list[Any], _v) + ] + if (_v := data.get("redirect_uris")) is not None + else None, + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UpdateOAuthApplication: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + if self.name is not None: + result["name"] = self.name + if self.description is not None: + result["description"] = self.description + else: + result["description"] = None + if self.scopes is not None: + result["scopes"] = self.scopes + else: + result["scopes"] = None + if self.redirect_uris is not None: + result["redirect_uris"] = [item.to_dict() for item in self.redirect_uris] + else: + result["redirect_uris"] = None + return result diff --git a/src/workos/async_client.py b/src/workos/async_client.py new file mode 100644 index 00000000..64db2170 --- /dev/null +++ b/src/workos/async_client.py @@ -0,0 +1,7 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._client import AsyncWorkOSClient, AsyncWorkOS + +AsyncClient = AsyncWorkOSClient + +__all__ = ["AsyncClient", "AsyncWorkOSClient", "AsyncWorkOS"] diff --git a/src/workos/audit_logs/__init__.py b/src/workos/audit_logs/__init__.py new file mode 100644 index 00000000..d9323927 --- /dev/null +++ b/src/workos/audit_logs/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import AuditLogs, AsyncAuditLogs +from .models import * diff --git a/src/workos/audit_logs/_resource.py b/src/workos/audit_logs/_resource.py new file mode 100644 index 00000000..b7e5c52e --- /dev/null +++ b/src/workos/audit_logs/_resource.py @@ -0,0 +1,609 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import ( + AuditLogActionJson, + AuditLogEventCreateResponse, + AuditLogEvent, + AuditLogExportJson, + AuditLogSchemaActor, + AuditLogSchemaJson, + AuditLogSchemaTarget, +) +from .models import AuditLogsOrder +from .._pagination import AsyncPage, SyncPage +from .._types import RequestOptions + + +class AuditLogs: + """Audit Logs API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[AuditLogsOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[AuditLogActionJson]: + """List Actions + + Get a list of all Audit Log actions in the current environment. + + Args: + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[AuditLogActionJson] + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path="audit_logs/actions", + model=AuditLogActionJson, + params=params, + request_options=request_options, + ) + + def list_schemas( + self, + action_name: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[AuditLogsOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[AuditLogSchemaJson]: + """List Schemas + + Get a list of all schemas for the Audit Logs action identified by `:name`. + + Args: + action_name: The name of the Audit Log action. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[AuditLogSchemaJson] + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path=f"audit_logs/actions/{action_name}/schemas", + model=AuditLogSchemaJson, + params=params, + request_options=request_options, + ) + + schemas = list_schemas + + def create_schema( + self, + action_name: str, + *, + targets: List[AuditLogSchemaTarget], + actor: Optional[AuditLogSchemaActor] = None, + metadata: Optional[Dict[str, Any]] = None, + request_options: Optional[RequestOptions] = None, + ) -> AuditLogSchemaJson: + """Create Schema + + Creates a new Audit Log schema used to validate the payload of incoming Audit Log Events. If the `action` does not exist, it will also be created. + + Args: + action_name: The name of the Audit Log action. + actor: The metadata schema for the actor. + targets: The list of targets for the schema. + metadata: Optional JSON schema for event metadata. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuditLogSchemaJson + + Raises: + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "actor": actor.to_dict() if actor is not None else None, + "targets": [item.to_dict() for item in targets], + "metadata": metadata, + }.items() + if v is not None + } + return self._client.request( + method="post", + path=f"audit_logs/actions/{action_name}/schemas", + body=body, + model=AuditLogSchemaJson, + request_options=request_options, + ) + + create_schemas = create_schema + + def create_event( + self, + *, + organization_id: str, + event: AuditLogEvent, + idempotency_key: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AuditLogEventCreateResponse: + """Create Event + + Create an Audit Log Event. + + This API supports idempotency which guarantees that performing the same operation multiple times will have the same result as if the operation were performed only once. This is handy in situations where you may need to retry a request due to a failure or prevent accidental duplicate requests from creating more than one resource. + + To achieve idempotency, you can add `Idempotency-Key` request header to a Create Event request with a unique string as the value. Each subsequent request matching this unique string will return the same response. We suggest using [v4 UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier) for idempotency keys to avoid collisions. + + Idempotency keys expire after 24 hours. The API will generate a new response if you submit a request with an expired key. + + Args: + organization_id: The unique ID of the Organization. + event: The audit log event to create. + idempotency_key: Optional idempotency key for safe retries. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuditLogEventCreateResponse + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + RateLimitExceededException: If rate limited (429). + AuthenticationException: If the API key is invalid (401). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "organization_id": organization_id, + "event": event.to_dict(), + } + return self._client.request( + method="post", + path="audit_logs/events", + body=body, + model=AuditLogEventCreateResponse, + idempotency_key=idempotency_key, + request_options=request_options, + ) + + create_events = create_event + + def exports( + self, + *, + organization_id: str, + range_start: str, + range_end: str, + actions: Optional[List[str]] = None, + actors: Optional[List[str]] = None, + actor_names: Optional[List[str]] = None, + actor_ids: Optional[List[str]] = None, + targets: Optional[List[str]] = None, + request_options: Optional[RequestOptions] = None, + ) -> AuditLogExportJson: + """Create Export + + Create an Audit Log Export. Exports are scoped to a single organization within a specified date range. + + Args: + organization_id: The unique ID of the Organization. + range_start: ISO-8601 value for start of the export range. + range_end: ISO-8601 value for end of the export range. + actions: List of actions to filter against. + actors: Deprecated. Use `actor_names` instead. + actor_names: List of actor names to filter against. + actor_ids: List of actor IDs to filter against. + targets: List of target types to filter against. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuditLogExportJson + + Raises: + BadRequestException: If the request is malformed (400). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "organization_id": organization_id, + "range_start": range_start, + "range_end": range_end, + "actions": actions, + "actors": actors, + "actor_names": actor_names, + "actor_ids": actor_ids, + "targets": targets, + }.items() + if v is not None + } + return self._client.request( + method="post", + path="audit_logs/exports", + body=body, + model=AuditLogExportJson, + request_options=request_options, + ) + + def export( + self, + audit_log_export_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> AuditLogExportJson: + """Get Export + + Get an Audit Log Export. The URL will expire after 10 minutes. If the export is needed again at a later time, refetching the export will regenerate the URL. + + Args: + audit_log_export_id: The unique ID of the Audit Log Export. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuditLogExportJson + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"audit_logs/exports/{audit_log_export_id}", + model=AuditLogExportJson, + request_options=request_options, + ) + + +class AsyncAuditLogs: + """Audit Logs API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[AuditLogsOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[AuditLogActionJson]: + """List Actions + + Get a list of all Audit Log actions in the current environment. + + Args: + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[AuditLogActionJson] + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path="audit_logs/actions", + model=AuditLogActionJson, + params=params, + request_options=request_options, + ) + + async def list_schemas( + self, + action_name: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[AuditLogsOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[AuditLogSchemaJson]: + """List Schemas + + Get a list of all schemas for the Audit Logs action identified by `:name`. + + Args: + action_name: The name of the Audit Log action. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[AuditLogSchemaJson] + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path=f"audit_logs/actions/{action_name}/schemas", + model=AuditLogSchemaJson, + params=params, + request_options=request_options, + ) + + schemas = list_schemas + + async def create_schema( + self, + action_name: str, + *, + targets: List[AuditLogSchemaTarget], + actor: Optional[AuditLogSchemaActor] = None, + metadata: Optional[Dict[str, Any]] = None, + request_options: Optional[RequestOptions] = None, + ) -> AuditLogSchemaJson: + """Create Schema + + Creates a new Audit Log schema used to validate the payload of incoming Audit Log Events. If the `action` does not exist, it will also be created. + + Args: + action_name: The name of the Audit Log action. + actor: The metadata schema for the actor. + targets: The list of targets for the schema. + metadata: Optional JSON schema for event metadata. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuditLogSchemaJson + + Raises: + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "actor": actor.to_dict() if actor is not None else None, + "targets": [item.to_dict() for item in targets], + "metadata": metadata, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path=f"audit_logs/actions/{action_name}/schemas", + body=body, + model=AuditLogSchemaJson, + request_options=request_options, + ) + + create_schemas = create_schema + + async def create_event( + self, + *, + organization_id: str, + event: AuditLogEvent, + idempotency_key: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AuditLogEventCreateResponse: + """Create Event + + Create an Audit Log Event. + + This API supports idempotency which guarantees that performing the same operation multiple times will have the same result as if the operation were performed only once. This is handy in situations where you may need to retry a request due to a failure or prevent accidental duplicate requests from creating more than one resource. + + To achieve idempotency, you can add `Idempotency-Key` request header to a Create Event request with a unique string as the value. Each subsequent request matching this unique string will return the same response. We suggest using [v4 UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier) for idempotency keys to avoid collisions. + + Idempotency keys expire after 24 hours. The API will generate a new response if you submit a request with an expired key. + + Args: + organization_id: The unique ID of the Organization. + event: The audit log event to create. + idempotency_key: Optional idempotency key for safe retries. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuditLogEventCreateResponse + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + RateLimitExceededException: If rate limited (429). + AuthenticationException: If the API key is invalid (401). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "organization_id": organization_id, + "event": event.to_dict(), + } + return await self._client.request( + method="post", + path="audit_logs/events", + body=body, + model=AuditLogEventCreateResponse, + idempotency_key=idempotency_key, + request_options=request_options, + ) + + create_events = create_event + + async def exports( + self, + *, + organization_id: str, + range_start: str, + range_end: str, + actions: Optional[List[str]] = None, + actors: Optional[List[str]] = None, + actor_names: Optional[List[str]] = None, + actor_ids: Optional[List[str]] = None, + targets: Optional[List[str]] = None, + request_options: Optional[RequestOptions] = None, + ) -> AuditLogExportJson: + """Create Export + + Create an Audit Log Export. Exports are scoped to a single organization within a specified date range. + + Args: + organization_id: The unique ID of the Organization. + range_start: ISO-8601 value for start of the export range. + range_end: ISO-8601 value for end of the export range. + actions: List of actions to filter against. + actors: Deprecated. Use `actor_names` instead. + actor_names: List of actor names to filter against. + actor_ids: List of actor IDs to filter against. + targets: List of target types to filter against. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuditLogExportJson + + Raises: + BadRequestException: If the request is malformed (400). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "organization_id": organization_id, + "range_start": range_start, + "range_end": range_end, + "actions": actions, + "actors": actors, + "actor_names": actor_names, + "actor_ids": actor_ids, + "targets": targets, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path="audit_logs/exports", + body=body, + model=AuditLogExportJson, + request_options=request_options, + ) + + async def export( + self, + audit_log_export_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> AuditLogExportJson: + """Get Export + + Get an Audit Log Export. The URL will expire after 10 minutes. If the export is needed again at a later time, refetching the export will regenerate the URL. + + Args: + audit_log_export_id: The unique ID of the Audit Log Export. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuditLogExportJson + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"audit_logs/exports/{audit_log_export_id}", + model=AuditLogExportJson, + request_options=request_options, + ) diff --git a/src/workos/audit_logs/models/__init__.py b/src/workos/audit_logs/models/__init__.py new file mode 100644 index 00000000..5f15d1e7 --- /dev/null +++ b/src/workos/audit_logs/models/__init__.py @@ -0,0 +1,24 @@ +# This file is auto-generated by oagen. Do not edit. + +from .audit_log_action_json import AuditLogActionJson as AuditLogActionJson +from .audit_log_event_actor import AuditLogEventActor as AuditLogEventActor +from .audit_log_event_context import AuditLogEventContext as AuditLogEventContext +from .audit_log_event_create_response import ( + AuditLogEventCreateResponse as AuditLogEventCreateResponse, +) +from .audit_log_event import AuditLogEvent as AuditLogEvent +from .audit_log_event_ingestion import AuditLogEventIngestion as AuditLogEventIngestion +from .audit_log_event_target import AuditLogEventTarget as AuditLogEventTarget +from .audit_log_export_creation import AuditLogExportCreation as AuditLogExportCreation +from .audit_log_export_json import AuditLogExportJson as AuditLogExportJson +from .audit_log_schema_actor import AuditLogSchemaActor as AuditLogSchemaActor +from .audit_log_schema import AuditLogSchema as AuditLogSchema +from .audit_log_schema_json import AuditLogSchemaJson as AuditLogSchemaJson +from .audit_log_schema_json_actor import ( + AuditLogSchemaJsonActor as AuditLogSchemaJsonActor, +) +from .audit_log_schema_json_target import ( + AuditLogSchemaJsonTarget as AuditLogSchemaJsonTarget, +) +from .audit_log_schema_target import AuditLogSchemaTarget as AuditLogSchemaTarget +from .audit_logs_order import AuditLogsOrder as AuditLogsOrder diff --git a/src/workos/audit_logs/models/audit_log_action_json.py b/src/workos/audit_logs/models/audit_log_action_json.py new file mode 100644 index 00000000..f1b67d2d --- /dev/null +++ b/src/workos/audit_logs/models/audit_log_action_json.py @@ -0,0 +1,62 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import cast +from typing import Any, Dict, Literal +from workos._errors import BaseRequestException + +from .audit_log_schema_json import AuditLogSchemaJson + + +@dataclass(slots=True) +class AuditLogActionJson: + """Audit Log Action Json model.""" + + object: Literal["audit_log_action"] + """Distinguishes the Audit Log Action object.""" + name: str + """Identifier of what action was taken.""" + schema: "AuditLogSchemaJson" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuditLogActionJson": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + name=data["name"], + schema=AuditLogSchemaJson.from_dict( + cast(Dict[str, Any], data["schema"]) + ), + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuditLogActionJson: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["name"] = self.name + result["schema"] = self.schema.to_dict() + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + return result diff --git a/src/workos/audit_logs/models/audit_log_event.py b/src/workos/audit_logs/models/audit_log_event.py new file mode 100644 index 00000000..18e91ecd --- /dev/null +++ b/src/workos/audit_logs/models/audit_log_event.py @@ -0,0 +1,69 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, List, Optional, Union +from workos._errors import BaseRequestException + +from .audit_log_event_actor import AuditLogEventActor +from .audit_log_event_context import AuditLogEventContext +from .audit_log_event_target import AuditLogEventTarget + + +@dataclass(slots=True) +class AuditLogEvent: + """Audit Log Event model.""" + + action: str + """Identifier of what happened.""" + occurred_at: str + """ISO-8601 value of when the action occurred.""" + actor: "AuditLogEventActor" + """The entity that performed the action.""" + targets: List["AuditLogEventTarget"] + """The resources affected by the action.""" + context: "AuditLogEventContext" + """Additional context about where and how the action occurred.""" + metadata: Optional[Dict[str, Union[str, float, bool]]] = None + """Additional data associated with the event or entity.""" + version: Optional[float] = None + """What schema version the event is associated with.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuditLogEvent": + """Deserialize from a dictionary.""" + try: + return cls( + action=data["action"], + occurred_at=data["occurred_at"], + actor=AuditLogEventActor.from_dict(cast(Dict[str, Any], data["actor"])), + targets=[ + AuditLogEventTarget.from_dict(cast(Dict[str, Any], item)) + for item in cast(list[Any], data["targets"]) + ], + context=AuditLogEventContext.from_dict( + cast(Dict[str, Any], data["context"]) + ), + metadata=data.get("metadata"), + version=data.get("version"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuditLogEvent: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["action"] = self.action + result["occurred_at"] = self.occurred_at + result["actor"] = self.actor.to_dict() + result["targets"] = [item.to_dict() for item in self.targets] + result["context"] = self.context.to_dict() + if self.metadata is not None: + result["metadata"] = self.metadata + if self.version is not None: + result["version"] = self.version + return result diff --git a/src/workos/audit_logs/models/audit_log_event_actor.py b/src/workos/audit_logs/models/audit_log_event_actor.py new file mode 100644 index 00000000..a0fa5fb1 --- /dev/null +++ b/src/workos/audit_logs/models/audit_log_event_actor.py @@ -0,0 +1,47 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional, Union +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AuditLogEventActor: + """Audit Log Event Actor model.""" + + id: str + """Actor identifier.""" + type: str + """Actor type.""" + name: Optional[str] = None + """Optional actor name.""" + metadata: Optional[Dict[str, Union[str, float, bool]]] = None + """Additional data associated with the event or entity.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuditLogEventActor": + """Deserialize from a dictionary.""" + try: + return cls( + id=data["id"], + type=data["type"], + name=data.get("name"), + metadata=data.get("metadata"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuditLogEventActor: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["id"] = self.id + result["type"] = self.type + if self.name is not None: + result["name"] = self.name + if self.metadata is not None: + result["metadata"] = self.metadata + return result diff --git a/src/workos/audit_logs/models/audit_log_event_context.py b/src/workos/audit_logs/models/audit_log_event_context.py new file mode 100644 index 00000000..8f2294c3 --- /dev/null +++ b/src/workos/audit_logs/models/audit_log_event_context.py @@ -0,0 +1,38 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AuditLogEventContext: + """Audit Log Event Context model.""" + + location: str + """IP Address or some other geolocation identifier.""" + user_agent: Optional[str] = None + """User agent string.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuditLogEventContext": + """Deserialize from a dictionary.""" + try: + return cls( + location=data["location"], + user_agent=data.get("user_agent"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuditLogEventContext: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["location"] = self.location + if self.user_agent is not None: + result["user_agent"] = self.user_agent + return result diff --git a/src/workos/audit_logs/models/audit_log_event_create_response.py b/src/workos/audit_logs/models/audit_log_event_create_response.py new file mode 100644 index 00000000..5462de20 --- /dev/null +++ b/src/workos/audit_logs/models/audit_log_event_create_response.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AuditLogEventCreateResponse: + """Audit Log Event Create Response model.""" + + success: bool + """Whether the Audit Log event was created successfully.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuditLogEventCreateResponse": + """Deserialize from a dictionary.""" + try: + return cls( + success=data["success"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuditLogEventCreateResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["success"] = self.success + return result diff --git a/src/workos/audit_logs/models/audit_log_event_ingestion.py b/src/workos/audit_logs/models/audit_log_event_ingestion.py new file mode 100644 index 00000000..e6572d5f --- /dev/null +++ b/src/workos/audit_logs/models/audit_log_event_ingestion.py @@ -0,0 +1,40 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict +from workos._errors import BaseRequestException + +from .audit_log_event import AuditLogEvent + + +@dataclass(slots=True) +class AuditLogEventIngestion: + """Audit Log Event Ingestion model.""" + + organization_id: str + """The unique ID of the Organization.""" + event: "AuditLogEvent" + """The audit log event to create.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuditLogEventIngestion": + """Deserialize from a dictionary.""" + try: + return cls( + organization_id=data["organization_id"], + event=AuditLogEvent.from_dict(cast(Dict[str, Any], data["event"])), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuditLogEventIngestion: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["organization_id"] = self.organization_id + result["event"] = self.event.to_dict() + return result diff --git a/src/workos/audit_logs/models/audit_log_event_target.py b/src/workos/audit_logs/models/audit_log_event_target.py new file mode 100644 index 00000000..c40f5224 --- /dev/null +++ b/src/workos/audit_logs/models/audit_log_event_target.py @@ -0,0 +1,47 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional, Union +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AuditLogEventTarget: + """Audit Log Event Target model.""" + + id: str + """Target identifier.""" + type: str + """Target type.""" + name: Optional[str] = None + """Optional target name.""" + metadata: Optional[Dict[str, Union[str, float, bool]]] = None + """Additional data associated with the event or entity.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuditLogEventTarget": + """Deserialize from a dictionary.""" + try: + return cls( + id=data["id"], + type=data["type"], + name=data.get("name"), + metadata=data.get("metadata"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuditLogEventTarget: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["id"] = self.id + result["type"] = self.type + if self.name is not None: + result["name"] = self.name + if self.metadata is not None: + result["metadata"] = self.metadata + return result diff --git a/src/workos/audit_logs/models/audit_log_export_creation.py b/src/workos/audit_logs/models/audit_log_export_creation.py new file mode 100644 index 00000000..a822ef14 --- /dev/null +++ b/src/workos/audit_logs/models/audit_log_export_creation.py @@ -0,0 +1,66 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, List, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AuditLogExportCreation: + """Audit Log Export Creation model.""" + + organization_id: str + """The unique ID of the Organization.""" + range_start: str + """ISO-8601 value for start of the export range.""" + range_end: str + """ISO-8601 value for end of the export range.""" + actions: Optional[List[str]] = None + """List of actions to filter against.""" + actors: Optional[List[str]] = None + """Deprecated. Use `actor_names` instead.""" + actor_names: Optional[List[str]] = None + """List of actor names to filter against.""" + actor_ids: Optional[List[str]] = None + """List of actor IDs to filter against.""" + targets: Optional[List[str]] = None + """List of target types to filter against.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuditLogExportCreation": + """Deserialize from a dictionary.""" + try: + return cls( + organization_id=data["organization_id"], + range_start=data["range_start"], + range_end=data["range_end"], + actions=data.get("actions"), + actors=data.get("actors"), + actor_names=data.get("actor_names"), + actor_ids=data.get("actor_ids"), + targets=data.get("targets"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuditLogExportCreation: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["organization_id"] = self.organization_id + result["range_start"] = self.range_start + result["range_end"] = self.range_end + if self.actions is not None: + result["actions"] = self.actions + if self.actors is not None: + result["actors"] = self.actors + if self.actor_names is not None: + result["actor_names"] = self.actor_names + if self.actor_ids is not None: + result["actor_ids"] = self.actor_ids + if self.targets is not None: + result["targets"] = self.targets + return result diff --git a/src/workos/audit_logs/models/audit_log_export_json.py b/src/workos/audit_logs/models/audit_log_export_json.py new file mode 100644 index 00000000..07367037 --- /dev/null +++ b/src/workos/audit_logs/models/audit_log_export_json.py @@ -0,0 +1,66 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException +from workos.common.models import AuditLogExportJsonState + + +@dataclass(slots=True) +class AuditLogExportJson: + """Audit Log Export Json model.""" + + object: Literal["audit_log_export"] + """Distinguishes the Audit Log Export object.""" + id: str + """The unique ID of the Audit Log Export.""" + state: "AuditLogExportJsonState" + """The state of the export. Possible values: pending, ready, error.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + url: Optional[str] = None + """A URL to the CSV file. Only defined when the Audit Log Export is ready.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuditLogExportJson": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + state=AuditLogExportJsonState(data["state"]), + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + url=data.get("url"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuditLogExportJson: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["state"] = self.state + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + if self.url is not None: + result["url"] = self.url + else: + result["url"] = None + return result diff --git a/src/workos/audit_logs/models/audit_log_schema.py b/src/workos/audit_logs/models/audit_log_schema.py new file mode 100644 index 00000000..f8293b00 --- /dev/null +++ b/src/workos/audit_logs/models/audit_log_schema.py @@ -0,0 +1,52 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, List, Optional +from workos._errors import BaseRequestException + +from .audit_log_schema_actor import AuditLogSchemaActor +from .audit_log_schema_target import AuditLogSchemaTarget + + +@dataclass(slots=True) +class AuditLogSchema: + """Audit Log Schema model.""" + + targets: List["AuditLogSchemaTarget"] + """The list of targets for the schema.""" + actor: Optional["AuditLogSchemaActor"] = None + """The metadata schema for the actor.""" + metadata: Optional[Dict[str, Any]] = None + """Optional JSON schema for event metadata.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuditLogSchema": + """Deserialize from a dictionary.""" + try: + return cls( + targets=[ + AuditLogSchemaTarget.from_dict(cast(Dict[str, Any], item)) + for item in cast(list[Any], data["targets"]) + ], + actor=AuditLogSchemaActor.from_dict(cast(Dict[str, Any], _v)) + if (_v := data.get("actor")) is not None + else None, + metadata=data.get("metadata"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuditLogSchema: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["targets"] = [item.to_dict() for item in self.targets] + if self.actor is not None: + result["actor"] = self.actor.to_dict() + if self.metadata is not None: + result["metadata"] = self.metadata + return result diff --git a/src/workos/audit_logs/models/audit_log_schema_actor.py b/src/workos/audit_logs/models/audit_log_schema_actor.py new file mode 100644 index 00000000..8054b5a2 --- /dev/null +++ b/src/workos/audit_logs/models/audit_log_schema_actor.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AuditLogSchemaActor: + """Audit Log Schema Actor model.""" + + metadata: Dict[str, Any] + """JSON schema for actor metadata.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuditLogSchemaActor": + """Deserialize from a dictionary.""" + try: + return cls( + metadata=data["metadata"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuditLogSchemaActor: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["metadata"] = self.metadata + return result diff --git a/src/workos/audit_logs/models/audit_log_schema_json.py b/src/workos/audit_logs/models/audit_log_schema_json.py new file mode 100644 index 00000000..0d0afba8 --- /dev/null +++ b/src/workos/audit_logs/models/audit_log_schema_json.py @@ -0,0 +1,69 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import cast +from typing import Any, Dict, List, Literal, Optional +from workos._errors import BaseRequestException + +from .audit_log_schema_json_actor import AuditLogSchemaJsonActor +from .audit_log_schema_json_target import AuditLogSchemaJsonTarget + + +@dataclass(slots=True) +class AuditLogSchemaJson: + """The schema associated with the action.""" + + object: Literal["audit_log_schema"] + """Distinguishes the Audit Log Schema object.""" + version: int + """The version of the schema.""" + targets: List["AuditLogSchemaJsonTarget"] + """The list of targets for the schema.""" + created_at: datetime + """The timestamp when the Audit Log Schema was created.""" + actor: Optional["AuditLogSchemaJsonActor"] = None + """The metadata schema for the actor.""" + metadata: Optional[Dict[str, Any]] = None + """Additional data associated with the event or entity.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuditLogSchemaJson": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + version=data["version"], + targets=[ + AuditLogSchemaJsonTarget.from_dict(cast(Dict[str, Any], item)) + for item in cast(list[Any], data["targets"]) + ], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + actor=AuditLogSchemaJsonActor.from_dict(cast(Dict[str, Any], _v)) + if (_v := data.get("actor")) is not None + else None, + metadata=data.get("metadata"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuditLogSchemaJson: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["version"] = self.version + result["targets"] = [item.to_dict() for item in self.targets] + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + if self.actor is not None: + result["actor"] = self.actor.to_dict() + if self.metadata is not None: + result["metadata"] = self.metadata + return result diff --git a/src/workos/audit_logs/models/audit_log_schema_json_actor.py b/src/workos/audit_logs/models/audit_log_schema_json_actor.py new file mode 100644 index 00000000..40594cc0 --- /dev/null +++ b/src/workos/audit_logs/models/audit_log_schema_json_actor.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from .audit_log_schema_actor import AuditLogSchemaActor + +AuditLogSchemaJsonActor = AuditLogSchemaActor diff --git a/src/workos/audit_logs/models/audit_log_schema_json_target.py b/src/workos/audit_logs/models/audit_log_schema_json_target.py new file mode 100644 index 00000000..ea9b6ecc --- /dev/null +++ b/src/workos/audit_logs/models/audit_log_schema_json_target.py @@ -0,0 +1,38 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AuditLogSchemaJsonTarget: + """Audit Log Schema Json Target model.""" + + type: str + """The type of the target resource.""" + metadata: Optional[Dict[str, Any]] = None + """Additional data associated with the event or entity.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuditLogSchemaJsonTarget": + """Deserialize from a dictionary.""" + try: + return cls( + type=data["type"], + metadata=data.get("metadata"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuditLogSchemaJsonTarget: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["type"] = self.type + if self.metadata is not None: + result["metadata"] = self.metadata + return result diff --git a/src/workos/audit_logs/models/audit_log_schema_target.py b/src/workos/audit_logs/models/audit_log_schema_target.py new file mode 100644 index 00000000..7d4becc9 --- /dev/null +++ b/src/workos/audit_logs/models/audit_log_schema_target.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from .audit_log_schema_json_target import AuditLogSchemaJsonTarget + +AuditLogSchemaTarget = AuditLogSchemaJsonTarget diff --git a/src/workos/audit_logs/models/audit_logs_order.py b/src/workos/audit_logs/models/audit_logs_order.py new file mode 100644 index 00000000..6182e83e --- /dev/null +++ b/src/workos/audit_logs/models/audit_logs_order.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import ApplicationsOrder + +AuditLogsOrder = ApplicationsOrder +__all__ = ["AuditLogsOrder"] diff --git a/src/workos/authorization/__init__.py b/src/workos/authorization/__init__.py new file mode 100644 index 00000000..467944e3 --- /dev/null +++ b/src/workos/authorization/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import Authorization, AsyncAuthorization +from .models import * diff --git a/src/workos/authorization/_resource.py b/src/workos/authorization/_resource.py new file mode 100644 index 00000000..16f05b4b --- /dev/null +++ b/src/workos/authorization/_resource.py @@ -0,0 +1,2747 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import ( + AuthorizationCheck, + AuthorizationResource, + ListModel, + Role, + RoleAssignment, + RoleList, + UserOrganizationMembershipBaseListData, +) +from .models import AuthorizationAssignment, AuthorizationOrder +from .._pagination import AsyncPage, SyncPage +from .._types import RequestOptions + + +class Authorization: + """Authorization API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def check( + self, + organization_membership_id: str, + *, + permission_slug: str, + resource_id: Optional[str] = None, + resource_external_id: Optional[str] = None, + resource_type_slug: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AuthorizationCheck: + """Check authorization + + Check if an organization membership has a specific permission on a resource. Supports identification by resource_id OR by resource_external_id + resource_type_slug. + + Args: + organization_membership_id: The ID of the organization membership to check. + permission_slug: The slug of the permission to check. + resource_id: The ID of the resource. + resource_external_id: The external ID of the resource. + resource_type_slug: The slug of the resource type. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthorizationCheck + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "permission_slug": permission_slug, + "resource_id": resource_id, + "resource_external_id": resource_external_id, + "resource_type_slug": resource_type_slug, + }.items() + if v is not None + } + return self._client.request( + method="post", + path=f"authorization/organization_memberships/{organization_membership_id}/check", + body=body, + model=AuthorizationCheck, + request_options=request_options, + ) + + def list_resources_for_membership( + self, + organization_membership_id: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[AuthorizationOrder] = None, + permission_slug: str, + parent_resource_id: Optional[str] = None, + parent_resource_type_slug: Optional[str] = None, + parent_resource_external_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[AuthorizationResource]: + """List resources for organization membership + + Returns all child resources of a parent resource where the organization membership has a specific permission. This is useful for resource discovery—answering "What projects can this user access in this workspace?" + + You must provide either `parent_resource_id` or both `parent_resource_external_id` and `parent_resource_type_slug` to identify the parent resource. + + Args: + organization_membership_id: The ID of the organization membership. + permission_slug: The permission slug to filter by. Only child resources where the organization membership has this permission are returned. + parent_resource_id: The WorkOS ID of the parent resource. Provide this or both `parent_resource_external_id` and `parent_resource_type_slug`, but not both. + parent_resource_type_slug: The slug of the parent resource type. Must be provided together with `parent_resource_external_id`. + parent_resource_external_id: The application-specific external identifier of the parent resource. Must be provided together with `parent_resource_type_slug`. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[AuthorizationResource] + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "permission_slug": permission_slug, + "parent_resource_id": parent_resource_id, + "parent_resource_type_slug": parent_resource_type_slug, + "parent_resource_external_id": parent_resource_external_id, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path=f"authorization/organization_memberships/{organization_membership_id}/resources", + model=AuthorizationResource, + params=params, + request_options=request_options, + ) + + def list_role_assignments( + self, + organization_membership_id: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[AuthorizationOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[RoleAssignment]: + """List role assignments + + List all role assignments for an organization membership. This returns all roles that have been assigned to the user on resources, including organization-level and sub-resource roles. + + Args: + organization_membership_id: The ID of the organization membership. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[RoleAssignment] + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path=f"authorization/organization_memberships/{organization_membership_id}/role_assignments", + model=RoleAssignment, + params=params, + request_options=request_options, + ) + + def assign_role( + self, + organization_membership_id: str, + *, + role_slug: str, + resource_id: Optional[str] = None, + resource_external_id: Optional[str] = None, + resource_type_slug: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> RoleAssignment: + """Assign a role + + Assign a role to an organization membership on a specific resource. + + Args: + organization_membership_id: The ID of the organization membership. + role_slug: The slug of the role to assign. + resource_id: The ID of the resource. Use either this or `resource_external_id` and `resource_type_slug`. + resource_external_id: The external ID of the resource. Requires `resource_type_slug`. + resource_type_slug: The resource type slug. Required with `resource_external_id`. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + RoleAssignment + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "role_slug": role_slug, + "resource_id": resource_id, + "resource_external_id": resource_external_id, + "resource_type_slug": resource_type_slug, + }.items() + if v is not None + } + return self._client.request( + method="post", + path=f"authorization/organization_memberships/{organization_membership_id}/role_assignments", + body=body, + model=RoleAssignment, + request_options=request_options, + ) + + def remove_role( + self, + organization_membership_id: str, + *, + role_slug: str, + resource_id: Optional[str] = None, + resource_external_id: Optional[str] = None, + resource_type_slug: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Remove a role assignment + + Remove a role assignment by role slug and resource. + + Args: + organization_membership_id: The ID of the organization membership. + role_slug: The slug of the role to remove. + resource_id: The ID of the resource. Use either this or `resource_external_id` and `resource_type_slug`. + resource_external_id: The external ID of the resource. Requires `resource_type_slug`. + resource_type_slug: The resource type slug. Required with `resource_external_id`. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "role_slug": role_slug, + "resource_id": resource_id, + "resource_external_id": resource_external_id, + "resource_type_slug": resource_type_slug, + }.items() + if v is not None + } + self._client.request( + method="delete", + path=f"authorization/organization_memberships/{organization_membership_id}/role_assignments", + body=body, + request_options=request_options, + ) + + remove_role_by_criteria = remove_role + + def remove_role_by_id( + self, + organization_membership_id: str, + role_assignment_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Remove a role assignment by ID + + Remove a role assignment using its ID. + + Args: + organization_membership_id: The ID of the organization membership. + role_assignment_id: The ID of the role assignment to remove. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + self._client.request( + method="delete", + path=f"authorization/organization_memberships/{organization_membership_id}/role_assignments/{role_assignment_id}", + request_options=request_options, + ) + + def list_roles_organizations( + self, + organization_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> ListModel: + """List organization roles + + Get a list of all roles that apply to an organization. This includes both environment roles and organization-specific roles, returned in priority order. + + Args: + organization_id: The ID of the organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + ListModel + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"authorization/organizations/{organization_id}/roles", + model=ListModel, + request_options=request_options, + ) + + def create_roles_organizations( + self, + organization_id: str, + *, + slug: str, + name: str, + description: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> Role: + """Create a custom organization role + + Create a new custom organization role. + + Args: + organization_id: The ID of the organization. + slug: A unique identifier for the role within the organization. Must begin with 'org-' and contain only lowercase letters, numbers, hyphens, and underscores. + name: A descriptive name for the role. + description: An optional description of the role's purpose. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Role + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "slug": slug, + "name": name, + "description": description, + }.items() + if v is not None + } + return self._client.request( + method="post", + path=f"authorization/organizations/{organization_id}/roles", + body=body, + model=Role, + request_options=request_options, + ) + + def get_roles_organization( + self, + organization_id: str, + slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Role: + """Get an organization role + + Retrieve a role that applies to an organization by its slug. This can return either an environment role or an organization-specific role. + + Args: + organization_id: The ID of the organization. + slug: The slug of the role. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Role + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"authorization/organizations/{organization_id}/roles/{slug}", + model=Role, + request_options=request_options, + ) + + get_roles_organizations = get_roles_organization + + def update_roles_organizations( + self, + organization_id: str, + slug: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> Role: + """Update an organization role + + Update an existing custom organization role. Only the fields provided in the request body will be updated. + + Args: + organization_id: The ID of the organization. + slug: The slug of the role. + name: A descriptive name for the role. + description: An optional description of the role's purpose. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Role + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "name": name, + "description": description, + }.items() + if v is not None + } + return self._client.request( + method="patch", + path=f"authorization/organizations/{organization_id}/roles/{slug}", + body=body, + model=Role, + request_options=request_options, + ) + + def delete_roles( + self, + organization_id: str, + slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a custom organization role + + Delete an existing custom organization role. + + Args: + organization_id: The ID of the organization. + slug: The slug of the role. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + self._client.request( + method="delete", + path=f"authorization/organizations/{organization_id}/roles/{slug}", + request_options=request_options, + ) + + def add_permission_permissions_organizations_roles( + self, + organization_id: str, + slug: str, + *, + body_slug: str, + request_options: Optional[RequestOptions] = None, + ) -> Role: + """Add a permission to an organization role + + Add a single permission to an organization role. If the permission is already assigned to the role, this operation has no effect. + + Args: + organization_id: The ID of the organization. + slug: The slug of the role. + body_slug: The slug of the permission to add to the role. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Role + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "slug": body_slug, + } + return self._client.request( + method="post", + path=f"authorization/organizations/{organization_id}/roles/{slug}/permissions", + body=body, + model=Role, + request_options=request_options, + ) + + def set_permissions_permissions_organizations_roles( + self, + organization_id: str, + slug: str, + *, + permissions: List[str], + request_options: Optional[RequestOptions] = None, + ) -> Role: + """Set permissions for a role + + Replace all permissions on a role with the provided list. + + Args: + organization_id: The ID of the organization. + slug: The slug of the role. + permissions: The permission slugs to assign to the role. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Role + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "permissions": permissions, + } + return self._client.request( + method="put", + path=f"authorization/organizations/{organization_id}/roles/{slug}/permissions", + body=body, + model=Role, + request_options=request_options, + ) + + def remove_permission( + self, + organization_id: str, + slug: str, + permission_slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Remove a permission from an organization role + + Remove a single permission from an organization role by its slug. + + Args: + organization_id: The ID of the organization. + slug: The slug of the role. + permission_slug: The slug of the permission to remove. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + self._client.request( + method="delete", + path=f"authorization/organizations/{organization_id}/roles/{slug}/permissions/{permission_slug}", + request_options=request_options, + ) + + def get_by_external_id( + self, + organization_id: str, + resource_type_slug: str, + external_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> AuthorizationResource: + """Get a resource by external ID + + Retrieve the details of an authorization resource by its external ID, organization, and resource type. This is useful when you only have the external ID from your system and need to fetch the full resource details. + + Args: + organization_id: The ID of the organization that owns the resource. + resource_type_slug: The slug of the resource type. + external_id: An identifier you provide to reference the resource in your system. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthorizationResource + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"authorization/organizations/{organization_id}/resources/{resource_type_slug}/{external_id}", + model=AuthorizationResource, + request_options=request_options, + ) + + def update_by_external_id( + self, + organization_id: str, + resource_type_slug: str, + external_id: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + parent_resource_id: Optional[str] = None, + parent_resource_external_id: Optional[str] = None, + parent_resource_type_slug: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AuthorizationResource: + """Update a resource by external ID + + Update an existing authorization resource using its external ID. + + Args: + organization_id: The ID of the organization that owns the resource. + resource_type_slug: The slug of the resource type. + external_id: An identifier you provide to reference the resource in your system. + name: A display name for the resource. + description: An optional description of the resource. + parent_resource_id: The ID of the parent resource. + parent_resource_external_id: The external ID of the parent resource. + parent_resource_type_slug: The resource type slug of the parent resource. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthorizationResource + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "name": name, + "description": description, + "parent_resource_id": parent_resource_id, + "parent_resource_external_id": parent_resource_external_id, + "parent_resource_type_slug": parent_resource_type_slug, + }.items() + if v is not None + } + return self._client.request( + method="patch", + path=f"authorization/organizations/{organization_id}/resources/{resource_type_slug}/{external_id}", + body=body, + model=AuthorizationResource, + request_options=request_options, + ) + + def delete_by_external_id( + self, + organization_id: str, + resource_type_slug: str, + external_id: str, + *, + cascade_delete: Optional[bool] = None, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete an authorization resource by external ID + + Delete an authorization resource by organization, resource type, and external ID. This also deletes all descendant resources. + + Args: + organization_id: The ID of the organization that owns the resource. + resource_type_slug: The slug of the resource type. + external_id: An identifier you provide to reference the resource in your system. + cascade_delete: If true, deletes all descendant resources and role assignments. If not set and the resource has children or assignments, the request will fail. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params: Dict[str, Any] = { + k: v + for k, v in { + "cascade_delete": cascade_delete, + }.items() + if v is not None + } + self._client.request( + method="delete", + path=f"authorization/organizations/{organization_id}/resources/{resource_type_slug}/{external_id}", + params=params, + request_options=request_options, + ) + + def list_organization_memberships_for_resource_by_external_id( + self, + organization_id: str, + resource_type_slug: str, + external_id: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[AuthorizationOrder] = None, + permission_slug: str, + assignment: Optional[AuthorizationAssignment] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[UserOrganizationMembershipBaseListData]: + """List memberships for a resource by external ID + + Returns all organization memberships that have a specific permission on a resource, using the resource's external ID. This is useful for answering "Who can access this resource?" when you only have the external ID. + + Args: + organization_id: The ID of the organization that owns the resource. + resource_type_slug: The slug of the resource type this resource belongs to. + external_id: An identifier you provide to reference the resource in your system. + permission_slug: The permission slug to filter by. Only users with this permission on the resource are returned. + assignment: Filter by assignment type. Use "direct" for direct assignments only, or "indirect" to include inherited assignments. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[UserOrganizationMembershipBaseListData] + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "permission_slug": permission_slug, + "assignment": assignment.value if assignment else None, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path=f"authorization/organizations/{organization_id}/resources/{resource_type_slug}/{external_id}/organization_memberships", + model=UserOrganizationMembershipBaseListData, + params=params, + request_options=request_options, + ) + + def list_resources( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[AuthorizationOrder] = None, + organization_id: Optional[str] = None, + resource_type_slug: Optional[str] = None, + parent_resource_id: Optional[str] = None, + parent_resource_type_slug: Optional[str] = None, + parent_external_id: Optional[str] = None, + search: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[AuthorizationResource]: + """List resources + + Get a paginated list of authorization resources. + + Args: + organization_id: Filter resources by organization ID. + resource_type_slug: Filter resources by resource type slug. + parent_resource_id: Filter resources by parent resource ID. + parent_resource_type_slug: Filter resources by parent resource type slug. + parent_external_id: Filter resources by parent external ID. + search: Search resources by name. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[AuthorizationResource] + + Raises: + AuthorizationException: If the request is forbidden (403). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "organization_id": organization_id, + "resource_type_slug": resource_type_slug, + "parent_resource_id": parent_resource_id, + "parent_resource_type_slug": parent_resource_type_slug, + "parent_external_id": parent_external_id, + "search": search, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path="authorization/resources", + model=AuthorizationResource, + params=params, + request_options=request_options, + ) + + def create_resource( + self, + *, + external_id: str, + name: str, + resource_type_slug: str, + organization_id: str, + description: Optional[str] = None, + parent_resource_id: Optional[str] = None, + parent_resource_external_id: Optional[str] = None, + parent_resource_type_slug: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AuthorizationResource: + """Create an authorization resource + + Create a new authorization resource. + + Args: + external_id: An external identifier for the resource. + name: A display name for the resource. + description: An optional description of the resource. + resource_type_slug: The slug of the resource type. + organization_id: The ID of the organization this resource belongs to. + parent_resource_id: The ID of the parent resource. + parent_resource_external_id: The external ID of the parent resource. + parent_resource_type_slug: The resource type slug of the parent resource. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthorizationResource + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "external_id": external_id, + "name": name, + "description": description, + "resource_type_slug": resource_type_slug, + "organization_id": organization_id, + "parent_resource_id": parent_resource_id, + "parent_resource_external_id": parent_resource_external_id, + "parent_resource_type_slug": parent_resource_type_slug, + }.items() + if v is not None + } + return self._client.request( + method="post", + path="authorization/resources", + body=body, + model=AuthorizationResource, + request_options=request_options, + ) + + create_resources = create_resource + + def get_by_id( + self, + resource_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> AuthorizationResource: + """Get a resource + + Retrieve the details of an authorization resource by its ID. + + Args: + resource_id: The ID of the authorization resource. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthorizationResource + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"authorization/resources/{resource_id}", + model=AuthorizationResource, + request_options=request_options, + ) + + find_by_id = get_by_id + + def update_resource( + self, + resource_id: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + parent_resource_id: Optional[str] = None, + parent_resource_external_id: Optional[str] = None, + parent_resource_type_slug: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AuthorizationResource: + """Update a resource + + Update an existing authorization resource. + + Args: + resource_id: The ID of the authorization resource. + name: A display name for the resource. + description: An optional description of the resource. + parent_resource_id: The ID of the parent resource. + parent_resource_external_id: The external ID of the parent resource. + parent_resource_type_slug: The resource type slug of the parent resource. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthorizationResource + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "name": name, + "description": description, + "parent_resource_id": parent_resource_id, + "parent_resource_external_id": parent_resource_external_id, + "parent_resource_type_slug": parent_resource_type_slug, + }.items() + if v is not None + } + return self._client.request( + method="patch", + path=f"authorization/resources/{resource_id}", + body=body, + model=AuthorizationResource, + request_options=request_options, + ) + + update_resources = update_resource + + def delete_resource( + self, + resource_id: str, + *, + cascade_delete: Optional[bool] = None, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete an authorization resource + + Delete an authorization resource and all its descendants. + + Args: + resource_id: The ID of the authorization resource. + cascade_delete: If true, deletes all descendant resources and role assignments. If not set and the resource has children or assignments, the request will fail. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params: Dict[str, Any] = { + k: v + for k, v in { + "cascade_delete": cascade_delete, + }.items() + if v is not None + } + self._client.request( + method="delete", + path=f"authorization/resources/{resource_id}", + params=params, + request_options=request_options, + ) + + delete_resources = delete_resource + + def list_organization_memberships_for_resource( + self, + resource_id: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[AuthorizationOrder] = None, + permission_slug: str, + assignment: Optional[AuthorizationAssignment] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[UserOrganizationMembershipBaseListData]: + """List organization memberships for resource + + Returns all organization memberships that have a specific permission on a resource instance. This is useful for answering "Who can access this resource?". + + Args: + resource_id: The ID of the authorization resource. + permission_slug: The permission slug to filter by. Only users with this permission on the resource are returned. + assignment: Filter by assignment type. Use `direct` for direct assignments only, or `indirect` to include inherited assignments. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[UserOrganizationMembershipBaseListData] + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "permission_slug": permission_slug, + "assignment": assignment.value if assignment else None, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path=f"authorization/resources/{resource_id}/organization_memberships", + model=UserOrganizationMembershipBaseListData, + params=params, + request_options=request_options, + ) + + def list_roles( + self, + *, + request_options: Optional[RequestOptions] = None, + ) -> RoleList: + """List environment roles + + List all environment roles in priority order. + + Returns: + RoleList + + Raises: + AuthorizationException: If the request is forbidden (403). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path="authorization/roles", + model=RoleList, + request_options=request_options, + ) + + def create_roles( + self, + *, + slug: str, + name: str, + description: Optional[str] = None, + resource_type_slug: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> Role: + """Create an environment role + + Create a new environment role. + + Args: + slug: A unique slug for the role. + name: A descriptive name for the role. + description: An optional description of the role. + resource_type_slug: The slug of the resource type the role is scoped to. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Role + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "slug": slug, + "name": name, + "description": description, + "resource_type_slug": resource_type_slug, + }.items() + if v is not None + } + return self._client.request( + method="post", + path="authorization/roles", + body=body, + model=Role, + request_options=request_options, + ) + + def get_roles( + self, + slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Role: + """Get an environment role + + Get an environment role by its slug. + + Args: + slug: The slug of the environment role. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Role + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"authorization/roles/{slug}", + model=Role, + request_options=request_options, + ) + + def update_roles( + self, + slug: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> Role: + """Update an environment role + + Update an existing environment role. + + Args: + slug: The slug of the environment role. + name: A descriptive name for the role. + description: An optional description of the role. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Role + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "name": name, + "description": description, + }.items() + if v is not None + } + return self._client.request( + method="patch", + path=f"authorization/roles/{slug}", + body=body, + model=Role, + request_options=request_options, + ) + + def add_permission_permissions_roles( + self, + slug: str, + *, + body_slug: str, + request_options: Optional[RequestOptions] = None, + ) -> Role: + """Add a permission to an environment role + + Add a single permission to an environment role. If the permission is already assigned to the role, this operation has no effect. + + Args: + slug: The slug of the environment role. + body_slug: The slug of the permission to add to the role. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Role + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "slug": body_slug, + } + return self._client.request( + method="post", + path=f"authorization/roles/{slug}/permissions", + body=body, + model=Role, + request_options=request_options, + ) + + def set_permissions_permissions_roles( + self, + slug: str, + *, + permissions: List[str], + request_options: Optional[RequestOptions] = None, + ) -> Role: + """Set permissions for an environment role + + Replace all permissions on an environment role with the provided list. + + Args: + slug: The slug of the environment role. + permissions: The permission slugs to assign to the role. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Role + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "permissions": permissions, + } + return self._client.request( + method="put", + path=f"authorization/roles/{slug}/permissions", + body=body, + model=Role, + request_options=request_options, + ) + + +class AsyncAuthorization: + """Authorization API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def check( + self, + organization_membership_id: str, + *, + permission_slug: str, + resource_id: Optional[str] = None, + resource_external_id: Optional[str] = None, + resource_type_slug: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AuthorizationCheck: + """Check authorization + + Check if an organization membership has a specific permission on a resource. Supports identification by resource_id OR by resource_external_id + resource_type_slug. + + Args: + organization_membership_id: The ID of the organization membership to check. + permission_slug: The slug of the permission to check. + resource_id: The ID of the resource. + resource_external_id: The external ID of the resource. + resource_type_slug: The slug of the resource type. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthorizationCheck + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "permission_slug": permission_slug, + "resource_id": resource_id, + "resource_external_id": resource_external_id, + "resource_type_slug": resource_type_slug, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path=f"authorization/organization_memberships/{organization_membership_id}/check", + body=body, + model=AuthorizationCheck, + request_options=request_options, + ) + + async def list_resources_for_membership( + self, + organization_membership_id: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[AuthorizationOrder] = None, + permission_slug: str, + parent_resource_id: Optional[str] = None, + parent_resource_type_slug: Optional[str] = None, + parent_resource_external_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[AuthorizationResource]: + """List resources for organization membership + + Returns all child resources of a parent resource where the organization membership has a specific permission. This is useful for resource discovery—answering "What projects can this user access in this workspace?" + + You must provide either `parent_resource_id` or both `parent_resource_external_id` and `parent_resource_type_slug` to identify the parent resource. + + Args: + organization_membership_id: The ID of the organization membership. + permission_slug: The permission slug to filter by. Only child resources where the organization membership has this permission are returned. + parent_resource_id: The WorkOS ID of the parent resource. Provide this or both `parent_resource_external_id` and `parent_resource_type_slug`, but not both. + parent_resource_type_slug: The slug of the parent resource type. Must be provided together with `parent_resource_external_id`. + parent_resource_external_id: The application-specific external identifier of the parent resource. Must be provided together with `parent_resource_type_slug`. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[AuthorizationResource] + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "permission_slug": permission_slug, + "parent_resource_id": parent_resource_id, + "parent_resource_type_slug": parent_resource_type_slug, + "parent_resource_external_id": parent_resource_external_id, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path=f"authorization/organization_memberships/{organization_membership_id}/resources", + model=AuthorizationResource, + params=params, + request_options=request_options, + ) + + async def list_role_assignments( + self, + organization_membership_id: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[AuthorizationOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[RoleAssignment]: + """List role assignments + + List all role assignments for an organization membership. This returns all roles that have been assigned to the user on resources, including organization-level and sub-resource roles. + + Args: + organization_membership_id: The ID of the organization membership. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[RoleAssignment] + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path=f"authorization/organization_memberships/{organization_membership_id}/role_assignments", + model=RoleAssignment, + params=params, + request_options=request_options, + ) + + async def assign_role( + self, + organization_membership_id: str, + *, + role_slug: str, + resource_id: Optional[str] = None, + resource_external_id: Optional[str] = None, + resource_type_slug: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> RoleAssignment: + """Assign a role + + Assign a role to an organization membership on a specific resource. + + Args: + organization_membership_id: The ID of the organization membership. + role_slug: The slug of the role to assign. + resource_id: The ID of the resource. Use either this or `resource_external_id` and `resource_type_slug`. + resource_external_id: The external ID of the resource. Requires `resource_type_slug`. + resource_type_slug: The resource type slug. Required with `resource_external_id`. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + RoleAssignment + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "role_slug": role_slug, + "resource_id": resource_id, + "resource_external_id": resource_external_id, + "resource_type_slug": resource_type_slug, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path=f"authorization/organization_memberships/{organization_membership_id}/role_assignments", + body=body, + model=RoleAssignment, + request_options=request_options, + ) + + async def remove_role( + self, + organization_membership_id: str, + *, + role_slug: str, + resource_id: Optional[str] = None, + resource_external_id: Optional[str] = None, + resource_type_slug: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Remove a role assignment + + Remove a role assignment by role slug and resource. + + Args: + organization_membership_id: The ID of the organization membership. + role_slug: The slug of the role to remove. + resource_id: The ID of the resource. Use either this or `resource_external_id` and `resource_type_slug`. + resource_external_id: The external ID of the resource. Requires `resource_type_slug`. + resource_type_slug: The resource type slug. Required with `resource_external_id`. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "role_slug": role_slug, + "resource_id": resource_id, + "resource_external_id": resource_external_id, + "resource_type_slug": resource_type_slug, + }.items() + if v is not None + } + await self._client.request( + method="delete", + path=f"authorization/organization_memberships/{organization_membership_id}/role_assignments", + body=body, + request_options=request_options, + ) + + remove_role_by_criteria = remove_role + + async def remove_role_by_id( + self, + organization_membership_id: str, + role_assignment_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Remove a role assignment by ID + + Remove a role assignment using its ID. + + Args: + organization_membership_id: The ID of the organization membership. + role_assignment_id: The ID of the role assignment to remove. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + await self._client.request( + method="delete", + path=f"authorization/organization_memberships/{organization_membership_id}/role_assignments/{role_assignment_id}", + request_options=request_options, + ) + + async def list_roles_organizations( + self, + organization_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> ListModel: + """List organization roles + + Get a list of all roles that apply to an organization. This includes both environment roles and organization-specific roles, returned in priority order. + + Args: + organization_id: The ID of the organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + ListModel + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"authorization/organizations/{organization_id}/roles", + model=ListModel, + request_options=request_options, + ) + + async def create_roles_organizations( + self, + organization_id: str, + *, + slug: str, + name: str, + description: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> Role: + """Create a custom organization role + + Create a new custom organization role. + + Args: + organization_id: The ID of the organization. + slug: A unique identifier for the role within the organization. Must begin with 'org-' and contain only lowercase letters, numbers, hyphens, and underscores. + name: A descriptive name for the role. + description: An optional description of the role's purpose. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Role + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "slug": slug, + "name": name, + "description": description, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path=f"authorization/organizations/{organization_id}/roles", + body=body, + model=Role, + request_options=request_options, + ) + + async def get_roles_organization( + self, + organization_id: str, + slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Role: + """Get an organization role + + Retrieve a role that applies to an organization by its slug. This can return either an environment role or an organization-specific role. + + Args: + organization_id: The ID of the organization. + slug: The slug of the role. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Role + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"authorization/organizations/{organization_id}/roles/{slug}", + model=Role, + request_options=request_options, + ) + + get_roles_organizations = get_roles_organization + + async def update_roles_organizations( + self, + organization_id: str, + slug: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> Role: + """Update an organization role + + Update an existing custom organization role. Only the fields provided in the request body will be updated. + + Args: + organization_id: The ID of the organization. + slug: The slug of the role. + name: A descriptive name for the role. + description: An optional description of the role's purpose. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Role + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "name": name, + "description": description, + }.items() + if v is not None + } + return await self._client.request( + method="patch", + path=f"authorization/organizations/{organization_id}/roles/{slug}", + body=body, + model=Role, + request_options=request_options, + ) + + async def delete_roles( + self, + organization_id: str, + slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a custom organization role + + Delete an existing custom organization role. + + Args: + organization_id: The ID of the organization. + slug: The slug of the role. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + await self._client.request( + method="delete", + path=f"authorization/organizations/{organization_id}/roles/{slug}", + request_options=request_options, + ) + + async def add_permission_permissions_organizations_roles( + self, + organization_id: str, + slug: str, + *, + body_slug: str, + request_options: Optional[RequestOptions] = None, + ) -> Role: + """Add a permission to an organization role + + Add a single permission to an organization role. If the permission is already assigned to the role, this operation has no effect. + + Args: + organization_id: The ID of the organization. + slug: The slug of the role. + body_slug: The slug of the permission to add to the role. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Role + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "slug": body_slug, + } + return await self._client.request( + method="post", + path=f"authorization/organizations/{organization_id}/roles/{slug}/permissions", + body=body, + model=Role, + request_options=request_options, + ) + + async def set_permissions_permissions_organizations_roles( + self, + organization_id: str, + slug: str, + *, + permissions: List[str], + request_options: Optional[RequestOptions] = None, + ) -> Role: + """Set permissions for a role + + Replace all permissions on a role with the provided list. + + Args: + organization_id: The ID of the organization. + slug: The slug of the role. + permissions: The permission slugs to assign to the role. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Role + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "permissions": permissions, + } + return await self._client.request( + method="put", + path=f"authorization/organizations/{organization_id}/roles/{slug}/permissions", + body=body, + model=Role, + request_options=request_options, + ) + + async def remove_permission( + self, + organization_id: str, + slug: str, + permission_slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Remove a permission from an organization role + + Remove a single permission from an organization role by its slug. + + Args: + organization_id: The ID of the organization. + slug: The slug of the role. + permission_slug: The slug of the permission to remove. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + await self._client.request( + method="delete", + path=f"authorization/organizations/{organization_id}/roles/{slug}/permissions/{permission_slug}", + request_options=request_options, + ) + + async def get_by_external_id( + self, + organization_id: str, + resource_type_slug: str, + external_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> AuthorizationResource: + """Get a resource by external ID + + Retrieve the details of an authorization resource by its external ID, organization, and resource type. This is useful when you only have the external ID from your system and need to fetch the full resource details. + + Args: + organization_id: The ID of the organization that owns the resource. + resource_type_slug: The slug of the resource type. + external_id: An identifier you provide to reference the resource in your system. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthorizationResource + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"authorization/organizations/{organization_id}/resources/{resource_type_slug}/{external_id}", + model=AuthorizationResource, + request_options=request_options, + ) + + async def update_by_external_id( + self, + organization_id: str, + resource_type_slug: str, + external_id: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + parent_resource_id: Optional[str] = None, + parent_resource_external_id: Optional[str] = None, + parent_resource_type_slug: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AuthorizationResource: + """Update a resource by external ID + + Update an existing authorization resource using its external ID. + + Args: + organization_id: The ID of the organization that owns the resource. + resource_type_slug: The slug of the resource type. + external_id: An identifier you provide to reference the resource in your system. + name: A display name for the resource. + description: An optional description of the resource. + parent_resource_id: The ID of the parent resource. + parent_resource_external_id: The external ID of the parent resource. + parent_resource_type_slug: The resource type slug of the parent resource. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthorizationResource + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "name": name, + "description": description, + "parent_resource_id": parent_resource_id, + "parent_resource_external_id": parent_resource_external_id, + "parent_resource_type_slug": parent_resource_type_slug, + }.items() + if v is not None + } + return await self._client.request( + method="patch", + path=f"authorization/organizations/{organization_id}/resources/{resource_type_slug}/{external_id}", + body=body, + model=AuthorizationResource, + request_options=request_options, + ) + + async def delete_by_external_id( + self, + organization_id: str, + resource_type_slug: str, + external_id: str, + *, + cascade_delete: Optional[bool] = None, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete an authorization resource by external ID + + Delete an authorization resource by organization, resource type, and external ID. This also deletes all descendant resources. + + Args: + organization_id: The ID of the organization that owns the resource. + resource_type_slug: The slug of the resource type. + external_id: An identifier you provide to reference the resource in your system. + cascade_delete: If true, deletes all descendant resources and role assignments. If not set and the resource has children or assignments, the request will fail. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params: Dict[str, Any] = { + k: v + for k, v in { + "cascade_delete": cascade_delete, + }.items() + if v is not None + } + await self._client.request( + method="delete", + path=f"authorization/organizations/{organization_id}/resources/{resource_type_slug}/{external_id}", + params=params, + request_options=request_options, + ) + + async def list_organization_memberships_for_resource_by_external_id( + self, + organization_id: str, + resource_type_slug: str, + external_id: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[AuthorizationOrder] = None, + permission_slug: str, + assignment: Optional[AuthorizationAssignment] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[UserOrganizationMembershipBaseListData]: + """List memberships for a resource by external ID + + Returns all organization memberships that have a specific permission on a resource, using the resource's external ID. This is useful for answering "Who can access this resource?" when you only have the external ID. + + Args: + organization_id: The ID of the organization that owns the resource. + resource_type_slug: The slug of the resource type this resource belongs to. + external_id: An identifier you provide to reference the resource in your system. + permission_slug: The permission slug to filter by. Only users with this permission on the resource are returned. + assignment: Filter by assignment type. Use "direct" for direct assignments only, or "indirect" to include inherited assignments. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[UserOrganizationMembershipBaseListData] + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "permission_slug": permission_slug, + "assignment": assignment.value if assignment else None, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path=f"authorization/organizations/{organization_id}/resources/{resource_type_slug}/{external_id}/organization_memberships", + model=UserOrganizationMembershipBaseListData, + params=params, + request_options=request_options, + ) + + async def list_resources( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[AuthorizationOrder] = None, + organization_id: Optional[str] = None, + resource_type_slug: Optional[str] = None, + parent_resource_id: Optional[str] = None, + parent_resource_type_slug: Optional[str] = None, + parent_external_id: Optional[str] = None, + search: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[AuthorizationResource]: + """List resources + + Get a paginated list of authorization resources. + + Args: + organization_id: Filter resources by organization ID. + resource_type_slug: Filter resources by resource type slug. + parent_resource_id: Filter resources by parent resource ID. + parent_resource_type_slug: Filter resources by parent resource type slug. + parent_external_id: Filter resources by parent external ID. + search: Search resources by name. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[AuthorizationResource] + + Raises: + AuthorizationException: If the request is forbidden (403). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "organization_id": organization_id, + "resource_type_slug": resource_type_slug, + "parent_resource_id": parent_resource_id, + "parent_resource_type_slug": parent_resource_type_slug, + "parent_external_id": parent_external_id, + "search": search, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path="authorization/resources", + model=AuthorizationResource, + params=params, + request_options=request_options, + ) + + async def create_resource( + self, + *, + external_id: str, + name: str, + resource_type_slug: str, + organization_id: str, + description: Optional[str] = None, + parent_resource_id: Optional[str] = None, + parent_resource_external_id: Optional[str] = None, + parent_resource_type_slug: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AuthorizationResource: + """Create an authorization resource + + Create a new authorization resource. + + Args: + external_id: An external identifier for the resource. + name: A display name for the resource. + description: An optional description of the resource. + resource_type_slug: The slug of the resource type. + organization_id: The ID of the organization this resource belongs to. + parent_resource_id: The ID of the parent resource. + parent_resource_external_id: The external ID of the parent resource. + parent_resource_type_slug: The resource type slug of the parent resource. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthorizationResource + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "external_id": external_id, + "name": name, + "description": description, + "resource_type_slug": resource_type_slug, + "organization_id": organization_id, + "parent_resource_id": parent_resource_id, + "parent_resource_external_id": parent_resource_external_id, + "parent_resource_type_slug": parent_resource_type_slug, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path="authorization/resources", + body=body, + model=AuthorizationResource, + request_options=request_options, + ) + + create_resources = create_resource + + async def get_by_id( + self, + resource_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> AuthorizationResource: + """Get a resource + + Retrieve the details of an authorization resource by its ID. + + Args: + resource_id: The ID of the authorization resource. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthorizationResource + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"authorization/resources/{resource_id}", + model=AuthorizationResource, + request_options=request_options, + ) + + find_by_id = get_by_id + + async def update_resource( + self, + resource_id: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + parent_resource_id: Optional[str] = None, + parent_resource_external_id: Optional[str] = None, + parent_resource_type_slug: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AuthorizationResource: + """Update a resource + + Update an existing authorization resource. + + Args: + resource_id: The ID of the authorization resource. + name: A display name for the resource. + description: An optional description of the resource. + parent_resource_id: The ID of the parent resource. + parent_resource_external_id: The external ID of the parent resource. + parent_resource_type_slug: The resource type slug of the parent resource. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthorizationResource + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "name": name, + "description": description, + "parent_resource_id": parent_resource_id, + "parent_resource_external_id": parent_resource_external_id, + "parent_resource_type_slug": parent_resource_type_slug, + }.items() + if v is not None + } + return await self._client.request( + method="patch", + path=f"authorization/resources/{resource_id}", + body=body, + model=AuthorizationResource, + request_options=request_options, + ) + + update_resources = update_resource + + async def delete_resource( + self, + resource_id: str, + *, + cascade_delete: Optional[bool] = None, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete an authorization resource + + Delete an authorization resource and all its descendants. + + Args: + resource_id: The ID of the authorization resource. + cascade_delete: If true, deletes all descendant resources and role assignments. If not set and the resource has children or assignments, the request will fail. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params: Dict[str, Any] = { + k: v + for k, v in { + "cascade_delete": cascade_delete, + }.items() + if v is not None + } + await self._client.request( + method="delete", + path=f"authorization/resources/{resource_id}", + params=params, + request_options=request_options, + ) + + delete_resources = delete_resource + + async def list_organization_memberships_for_resource( + self, + resource_id: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[AuthorizationOrder] = None, + permission_slug: str, + assignment: Optional[AuthorizationAssignment] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[UserOrganizationMembershipBaseListData]: + """List organization memberships for resource + + Returns all organization memberships that have a specific permission on a resource instance. This is useful for answering "Who can access this resource?". + + Args: + resource_id: The ID of the authorization resource. + permission_slug: The permission slug to filter by. Only users with this permission on the resource are returned. + assignment: Filter by assignment type. Use `direct` for direct assignments only, or `indirect` to include inherited assignments. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[UserOrganizationMembershipBaseListData] + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "permission_slug": permission_slug, + "assignment": assignment.value if assignment else None, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path=f"authorization/resources/{resource_id}/organization_memberships", + model=UserOrganizationMembershipBaseListData, + params=params, + request_options=request_options, + ) + + async def list_roles( + self, + *, + request_options: Optional[RequestOptions] = None, + ) -> RoleList: + """List environment roles + + List all environment roles in priority order. + + Returns: + RoleList + + Raises: + AuthorizationException: If the request is forbidden (403). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path="authorization/roles", + model=RoleList, + request_options=request_options, + ) + + async def create_roles( + self, + *, + slug: str, + name: str, + description: Optional[str] = None, + resource_type_slug: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> Role: + """Create an environment role + + Create a new environment role. + + Args: + slug: A unique slug for the role. + name: A descriptive name for the role. + description: An optional description of the role. + resource_type_slug: The slug of the resource type the role is scoped to. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Role + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "slug": slug, + "name": name, + "description": description, + "resource_type_slug": resource_type_slug, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path="authorization/roles", + body=body, + model=Role, + request_options=request_options, + ) + + async def get_roles( + self, + slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Role: + """Get an environment role + + Get an environment role by its slug. + + Args: + slug: The slug of the environment role. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Role + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"authorization/roles/{slug}", + model=Role, + request_options=request_options, + ) + + async def update_roles( + self, + slug: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> Role: + """Update an environment role + + Update an existing environment role. + + Args: + slug: The slug of the environment role. + name: A descriptive name for the role. + description: An optional description of the role. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Role + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "name": name, + "description": description, + }.items() + if v is not None + } + return await self._client.request( + method="patch", + path=f"authorization/roles/{slug}", + body=body, + model=Role, + request_options=request_options, + ) + + async def add_permission_permissions_roles( + self, + slug: str, + *, + body_slug: str, + request_options: Optional[RequestOptions] = None, + ) -> Role: + """Add a permission to an environment role + + Add a single permission to an environment role. If the permission is already assigned to the role, this operation has no effect. + + Args: + slug: The slug of the environment role. + body_slug: The slug of the permission to add to the role. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Role + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "slug": body_slug, + } + return await self._client.request( + method="post", + path=f"authorization/roles/{slug}/permissions", + body=body, + model=Role, + request_options=request_options, + ) + + async def set_permissions_permissions_roles( + self, + slug: str, + *, + permissions: List[str], + request_options: Optional[RequestOptions] = None, + ) -> Role: + """Set permissions for an environment role + + Replace all permissions on an environment role with the provided list. + + Args: + slug: The slug of the environment role. + permissions: The permission slugs to assign to the role. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Role + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "permissions": permissions, + } + return await self._client.request( + method="put", + path=f"authorization/roles/{slug}/permissions", + body=body, + model=Role, + request_options=request_options, + ) diff --git a/src/workos/authorization/models/__init__.py b/src/workos/authorization/models/__init__.py new file mode 100644 index 00000000..dde98d68 --- /dev/null +++ b/src/workos/authorization/models/__init__.py @@ -0,0 +1,32 @@ +# This file is auto-generated by oagen. Do not edit. + +from .add_role_permission import AddRolePermission as AddRolePermission +from .assign_role import AssignRole as AssignRole +from .assignment import Assignment as Assignment +from .authorization_assignment import AuthorizationAssignment as AuthorizationAssignment +from .authorization_check import AuthorizationCheck as AuthorizationCheck +from .authorization_order import AuthorizationOrder as AuthorizationOrder +from .authorization_resource import AuthorizationResource as AuthorizationResource +from .check_authorization import CheckAuthorization as CheckAuthorization +from .create_authorization_resource import ( + CreateAuthorizationResource as CreateAuthorizationResource, +) +from .create_organization_role import CreateOrganizationRole as CreateOrganizationRole +from .create_role import CreateRole as CreateRole +from .list import ListModel as ListModel +from .list_data import ListData as ListData +from .remove_role import RemoveRole as RemoveRole +from .role import Role as Role +from .role_assignment import RoleAssignment as RoleAssignment +from .role_assignment_resource import RoleAssignmentResource as RoleAssignmentResource +from .role_list import RoleList as RoleList +from .set_role_permissions import SetRolePermissions as SetRolePermissions +from .slim_role import SlimRole as SlimRole +from .update_authorization_resource import ( + UpdateAuthorizationResource as UpdateAuthorizationResource, +) +from .update_organization_role import UpdateOrganizationRole as UpdateOrganizationRole +from .update_role import UpdateRole as UpdateRole +from .user_organization_membership_base_list_data import ( + UserOrganizationMembershipBaseListData as UserOrganizationMembershipBaseListData, +) diff --git a/src/workos/authorization/models/add_role_permission.py b/src/workos/authorization/models/add_role_permission.py new file mode 100644 index 00000000..e8d276dc --- /dev/null +++ b/src/workos/authorization/models/add_role_permission.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AddRolePermission: + """Add Role Permission model.""" + + slug: str + """The slug of the permission to add to the role.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AddRolePermission": + """Deserialize from a dictionary.""" + try: + return cls( + slug=data["slug"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AddRolePermission: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["slug"] = self.slug + return result diff --git a/src/workos/authorization/models/assign_role.py b/src/workos/authorization/models/assign_role.py new file mode 100644 index 00000000..6b6edae6 --- /dev/null +++ b/src/workos/authorization/models/assign_role.py @@ -0,0 +1,48 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AssignRole: + """Assign Role model.""" + + role_slug: str + """The slug of the role to assign.""" + resource_id: Optional[str] = None + """The ID of the resource. Use either this or `resource_external_id` and `resource_type_slug`.""" + resource_external_id: Optional[str] = None + """The external ID of the resource. Requires `resource_type_slug`.""" + resource_type_slug: Optional[str] = None + """The resource type slug. Required with `resource_external_id`.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AssignRole": + """Deserialize from a dictionary.""" + try: + return cls( + role_slug=data["role_slug"], + resource_id=data.get("resource_id"), + resource_external_id=data.get("resource_external_id"), + resource_type_slug=data.get("resource_type_slug"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AssignRole: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["role_slug"] = self.role_slug + if self.resource_id is not None: + result["resource_id"] = self.resource_id + if self.resource_external_id is not None: + result["resource_external_id"] = self.resource_external_id + if self.resource_type_slug is not None: + result["resource_type_slug"] = self.resource_type_slug + return result diff --git a/src/workos/authorization/models/assignment.py b/src/workos/authorization/models/assignment.py new file mode 100644 index 00000000..dc1d3074 --- /dev/null +++ b/src/workos/authorization/models/assignment.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .authorization_assignment import AuthorizationAssignment + +Assignment = AuthorizationAssignment +__all__ = ["Assignment"] diff --git a/src/workos/authorization/models/authorization_assignment.py b/src/workos/authorization/models/authorization_assignment.py new file mode 100644 index 00000000..272f5e2b --- /dev/null +++ b/src/workos/authorization/models/authorization_assignment.py @@ -0,0 +1,28 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of authorization assignment values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class AuthorizationAssignment(str, Enum): + """Known values for AuthorizationAssignment.""" + + DIRECT = "direct" + INDIRECT = "indirect" + + @classmethod + def _missing_(cls, value: object) -> Optional["AuthorizationAssignment"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +AuthorizationAssignmentLiteral: TypeAlias = Literal["direct", "indirect"] diff --git a/src/workos/authorization/models/authorization_check.py b/src/workos/authorization/models/authorization_check.py new file mode 100644 index 00000000..c4443b5e --- /dev/null +++ b/src/workos/authorization/models/authorization_check.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AuthorizationCheck: + """Authorization Check model.""" + + authorized: bool + """Whether the organization membership has the specified permission on the resource.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuthorizationCheck": + """Deserialize from a dictionary.""" + try: + return cls( + authorized=data["authorized"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuthorizationCheck: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["authorized"] = self.authorized + return result diff --git a/src/workos/authorization/models/authorization_order.py b/src/workos/authorization/models/authorization_order.py new file mode 100644 index 00000000..9206fa0c --- /dev/null +++ b/src/workos/authorization/models/authorization_order.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import ApplicationsOrder + +AuthorizationOrder = ApplicationsOrder +__all__ = ["AuthorizationOrder"] diff --git a/src/workos/authorization/models/authorization_resource.py b/src/workos/authorization/models/authorization_resource.py new file mode 100644 index 00000000..1c3d95a5 --- /dev/null +++ b/src/workos/authorization/models/authorization_resource.py @@ -0,0 +1,84 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AuthorizationResource: + """Authorization Resource model.""" + + object: Literal["authorization_resource"] + """Distinguishes the Resource object.""" + name: str + """A human-readable name for the Resource.""" + description: Optional[str] + """An optional description of the Resource.""" + organization_id: str + """The ID of the organization that owns the resource.""" + parent_resource_id: Optional[str] + """The ID of the parent resource, if this resource is nested.""" + id: str + """The unique ID of the Resource.""" + external_id: str + """An identifier you provide to reference the resource in your system.""" + resource_type_slug: str + """The slug of the resource type this resource belongs to.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuthorizationResource": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + name=data["name"], + description=data["description"], + organization_id=data["organization_id"], + parent_resource_id=data["parent_resource_id"], + id=data["id"], + external_id=data["external_id"], + resource_type_slug=data["resource_type_slug"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuthorizationResource: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["name"] = self.name + if self.description is not None: + result["description"] = self.description + else: + result["description"] = None + result["organization_id"] = self.organization_id + if self.parent_resource_id is not None: + result["parent_resource_id"] = self.parent_resource_id + else: + result["parent_resource_id"] = None + result["id"] = self.id + result["external_id"] = self.external_id + result["resource_type_slug"] = self.resource_type_slug + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + return result diff --git a/src/workos/authorization/models/check_authorization.py b/src/workos/authorization/models/check_authorization.py new file mode 100644 index 00000000..c6a3a9d2 --- /dev/null +++ b/src/workos/authorization/models/check_authorization.py @@ -0,0 +1,48 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class CheckAuthorization: + """Check Authorization model.""" + + permission_slug: str + """The slug of the permission to check.""" + resource_id: Optional[str] = None + """The ID of the resource.""" + resource_external_id: Optional[str] = None + """The external ID of the resource.""" + resource_type_slug: Optional[str] = None + """The slug of the resource type.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CheckAuthorization": + """Deserialize from a dictionary.""" + try: + return cls( + permission_slug=data["permission_slug"], + resource_id=data.get("resource_id"), + resource_external_id=data.get("resource_external_id"), + resource_type_slug=data.get("resource_type_slug"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing CheckAuthorization: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["permission_slug"] = self.permission_slug + if self.resource_id is not None: + result["resource_id"] = self.resource_id + if self.resource_external_id is not None: + result["resource_external_id"] = self.resource_external_id + if self.resource_type_slug is not None: + result["resource_type_slug"] = self.resource_type_slug + return result diff --git a/src/workos/authorization/models/create_authorization_resource.py b/src/workos/authorization/models/create_authorization_resource.py new file mode 100644 index 00000000..ef5dc64d --- /dev/null +++ b/src/workos/authorization/models/create_authorization_resource.py @@ -0,0 +1,69 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class CreateAuthorizationResource: + """Create Authorization Resource model.""" + + external_id: str + """An external identifier for the resource.""" + name: str + """A display name for the resource.""" + resource_type_slug: str + """The slug of the resource type.""" + organization_id: str + """The ID of the organization this resource belongs to.""" + description: Optional[str] = None + """An optional description of the resource.""" + parent_resource_id: Optional[str] = None + """The ID of the parent resource.""" + parent_resource_external_id: Optional[str] = None + """The external ID of the parent resource.""" + parent_resource_type_slug: Optional[str] = None + """The resource type slug of the parent resource.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CreateAuthorizationResource": + """Deserialize from a dictionary.""" + try: + return cls( + external_id=data["external_id"], + name=data["name"], + resource_type_slug=data["resource_type_slug"], + organization_id=data["organization_id"], + description=data.get("description"), + parent_resource_id=data.get("parent_resource_id"), + parent_resource_external_id=data.get("parent_resource_external_id"), + parent_resource_type_slug=data.get("parent_resource_type_slug"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing CreateAuthorizationResource: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["external_id"] = self.external_id + result["name"] = self.name + result["resource_type_slug"] = self.resource_type_slug + result["organization_id"] = self.organization_id + if self.description is not None: + result["description"] = self.description + else: + result["description"] = None + if self.parent_resource_id is not None: + result["parent_resource_id"] = self.parent_resource_id + else: + result["parent_resource_id"] = None + if self.parent_resource_external_id is not None: + result["parent_resource_external_id"] = self.parent_resource_external_id + if self.parent_resource_type_slug is not None: + result["parent_resource_type_slug"] = self.parent_resource_type_slug + return result diff --git a/src/workos/authorization/models/create_organization_role.py b/src/workos/authorization/models/create_organization_role.py new file mode 100644 index 00000000..fa80c0b6 --- /dev/null +++ b/src/workos/authorization/models/create_organization_role.py @@ -0,0 +1,44 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class CreateOrganizationRole: + """Create Organization Role model.""" + + slug: str + """A unique identifier for the role within the organization. Must begin with 'org-' and contain only lowercase letters, numbers, hyphens, and underscores.""" + name: str + """A descriptive name for the role.""" + description: Optional[str] = None + """An optional description of the role's purpose.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CreateOrganizationRole": + """Deserialize from a dictionary.""" + try: + return cls( + slug=data["slug"], + name=data["name"], + description=data.get("description"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing CreateOrganizationRole: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["slug"] = self.slug + result["name"] = self.name + if self.description is not None: + result["description"] = self.description + else: + result["description"] = None + return result diff --git a/src/workos/authorization/models/create_role.py b/src/workos/authorization/models/create_role.py new file mode 100644 index 00000000..ac850776 --- /dev/null +++ b/src/workos/authorization/models/create_role.py @@ -0,0 +1,49 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class CreateRole: + """Create Role model.""" + + slug: str + """A unique slug for the role.""" + name: str + """A descriptive name for the role.""" + description: Optional[str] = None + """An optional description of the role.""" + resource_type_slug: Optional[str] = None + """The slug of the resource type the role is scoped to.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CreateRole": + """Deserialize from a dictionary.""" + try: + return cls( + slug=data["slug"], + name=data["name"], + description=data.get("description"), + resource_type_slug=data.get("resource_type_slug"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing CreateRole: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["slug"] = self.slug + result["name"] = self.name + if self.description is not None: + result["description"] = self.description + else: + result["description"] = None + if self.resource_type_slug is not None: + result["resource_type_slug"] = self.resource_type_slug + return result diff --git a/src/workos/authorization/models/list.py b/src/workos/authorization/models/list.py new file mode 100644 index 00000000..a5ec9968 --- /dev/null +++ b/src/workos/authorization/models/list.py @@ -0,0 +1,42 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, List, Literal +from workos._errors import BaseRequestException + +from .list_data import ListData + + +@dataclass(slots=True) +class ListModel: + """List Model model.""" + + object: Literal["list"] + data: List["ListData"] + """The list of records for the current page.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ListModel": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + data=[ + ListData.from_dict(cast(Dict[str, Any], item)) + for item in cast(list[Any], data["data"]) + ], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing ListModel: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["data"] = [item.to_dict() for item in self.data] + return result diff --git a/src/workos/authorization/models/list_data.py b/src/workos/authorization/models/list_data.py new file mode 100644 index 00000000..afffba2f --- /dev/null +++ b/src/workos/authorization/models/list_data.py @@ -0,0 +1,82 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, List, Literal, Optional +from workos._errors import BaseRequestException +from workos.common.models import ListDataType + + +@dataclass(slots=True) +class ListData: + """List Data model.""" + + slug: str + """A unique slug for the role.""" + object: Literal["role"] + """Distinguishes the role object.""" + id: str + """Unique identifier of the role.""" + name: str + """A descriptive name for the role.""" + description: Optional[str] + """An optional description of the role.""" + type: "ListDataType" + """Whether the role is scoped to the environment or an organization.""" + resource_type_slug: str + """The slug of the resource type the role is scoped to.""" + permissions: List[str] + """The permission slugs assigned to the role.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ListData": + """Deserialize from a dictionary.""" + try: + return cls( + slug=data["slug"], + object=data["object"], + id=data["id"], + name=data["name"], + description=data["description"], + type=ListDataType(data["type"]), + resource_type_slug=data["resource_type_slug"], + permissions=data["permissions"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing ListData: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["slug"] = self.slug + result["object"] = self.object + result["id"] = self.id + result["name"] = self.name + if self.description is not None: + result["description"] = self.description + else: + result["description"] = None + result["type"] = self.type + result["resource_type_slug"] = self.resource_type_slug + result["permissions"] = self.permissions + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + return result diff --git a/src/workos/authorization/models/remove_role.py b/src/workos/authorization/models/remove_role.py new file mode 100644 index 00000000..def49f0d --- /dev/null +++ b/src/workos/authorization/models/remove_role.py @@ -0,0 +1,48 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class RemoveRole: + """Remove Role model.""" + + role_slug: str + """The slug of the role to remove.""" + resource_id: Optional[str] = None + """The ID of the resource. Use either this or `resource_external_id` and `resource_type_slug`.""" + resource_external_id: Optional[str] = None + """The external ID of the resource. Requires `resource_type_slug`.""" + resource_type_slug: Optional[str] = None + """The resource type slug. Required with `resource_external_id`.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "RemoveRole": + """Deserialize from a dictionary.""" + try: + return cls( + role_slug=data["role_slug"], + resource_id=data.get("resource_id"), + resource_external_id=data.get("resource_external_id"), + resource_type_slug=data.get("resource_type_slug"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing RemoveRole: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["role_slug"] = self.role_slug + if self.resource_id is not None: + result["resource_id"] = self.resource_id + if self.resource_external_id is not None: + result["resource_external_id"] = self.resource_external_id + if self.resource_type_slug is not None: + result["resource_type_slug"] = self.resource_type_slug + return result diff --git a/src/workos/authorization/models/role.py b/src/workos/authorization/models/role.py new file mode 100644 index 00000000..a31b6737 --- /dev/null +++ b/src/workos/authorization/models/role.py @@ -0,0 +1,82 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, List, Literal, Optional +from workos._errors import BaseRequestException +from workos.common.models import RoleType + + +@dataclass(slots=True) +class Role: + """Role model.""" + + slug: str + """A unique slug for the role.""" + object: Literal["role"] + """Distinguishes the role object.""" + id: str + """Unique identifier of the role.""" + name: str + """A descriptive name for the role.""" + description: Optional[str] + """An optional description of the role.""" + type: "RoleType" + """Whether the role is scoped to the environment or an organization.""" + resource_type_slug: str + """The slug of the resource type the role is scoped to.""" + permissions: List[str] + """The permission slugs assigned to the role.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Role": + """Deserialize from a dictionary.""" + try: + return cls( + slug=data["slug"], + object=data["object"], + id=data["id"], + name=data["name"], + description=data["description"], + type=RoleType(data["type"]), + resource_type_slug=data["resource_type_slug"], + permissions=data["permissions"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing Role: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["slug"] = self.slug + result["object"] = self.object + result["id"] = self.id + result["name"] = self.name + if self.description is not None: + result["description"] = self.description + else: + result["description"] = None + result["type"] = self.type + result["resource_type_slug"] = self.resource_type_slug + result["permissions"] = self.permissions + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + return result diff --git a/src/workos/authorization/models/role_assignment.py b/src/workos/authorization/models/role_assignment.py new file mode 100644 index 00000000..ead5b127 --- /dev/null +++ b/src/workos/authorization/models/role_assignment.py @@ -0,0 +1,67 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import cast +from typing import Any, Dict, Literal +from workos._errors import BaseRequestException + +from .role_assignment_resource import RoleAssignmentResource +from .slim_role import SlimRole + + +@dataclass(slots=True) +class RoleAssignment: + """Role Assignment model.""" + + object: Literal["role_assignment"] + """Distinguishes the role assignment object.""" + id: str + """Unique identifier of the role assignment.""" + role: "SlimRole" + resource: "RoleAssignmentResource" + """The resource to which the role is assigned.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "RoleAssignment": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + role=SlimRole.from_dict(cast(Dict[str, Any], data["role"])), + resource=RoleAssignmentResource.from_dict( + cast(Dict[str, Any], data["resource"]) + ), + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing RoleAssignment: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["role"] = self.role.to_dict() + result["resource"] = self.resource.to_dict() + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + return result diff --git a/src/workos/authorization/models/role_assignment_resource.py b/src/workos/authorization/models/role_assignment_resource.py new file mode 100644 index 00000000..c62316e1 --- /dev/null +++ b/src/workos/authorization/models/role_assignment_resource.py @@ -0,0 +1,41 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class RoleAssignmentResource: + """The resource to which the role is assigned.""" + + id: str + """The unique ID of the Resource.""" + external_id: str + """An identifier you provide to reference the resource in your system.""" + resource_type_slug: str + """The slug of the resource type this resource belongs to.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "RoleAssignmentResource": + """Deserialize from a dictionary.""" + try: + return cls( + id=data["id"], + external_id=data["external_id"], + resource_type_slug=data["resource_type_slug"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing RoleAssignmentResource: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["id"] = self.id + result["external_id"] = self.external_id + result["resource_type_slug"] = self.resource_type_slug + return result diff --git a/src/workos/authorization/models/role_list.py b/src/workos/authorization/models/role_list.py new file mode 100644 index 00000000..9e20241b --- /dev/null +++ b/src/workos/authorization/models/role_list.py @@ -0,0 +1,42 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, List, Literal +from workos._errors import BaseRequestException + +from .role import Role + + +@dataclass(slots=True) +class RoleList: + """Role List model.""" + + object: Literal["list"] + data: List["Role"] + """The list of records for the current page.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "RoleList": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + data=[ + Role.from_dict(cast(Dict[str, Any], item)) + for item in cast(list[Any], data["data"]) + ], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing RoleList: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["data"] = [item.to_dict() for item in self.data] + return result diff --git a/src/workos/authorization/models/set_role_permissions.py b/src/workos/authorization/models/set_role_permissions.py new file mode 100644 index 00000000..16fbf72a --- /dev/null +++ b/src/workos/authorization/models/set_role_permissions.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, List +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class SetRolePermissions: + """Set Role Permissions model.""" + + permissions: List[str] + """The permission slugs to assign to the role.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "SetRolePermissions": + """Deserialize from a dictionary.""" + try: + return cls( + permissions=data["permissions"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing SetRolePermissions: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["permissions"] = self.permissions + return result diff --git a/src/workos/authorization/models/slim_role.py b/src/workos/authorization/models/slim_role.py new file mode 100644 index 00000000..7738aeb0 --- /dev/null +++ b/src/workos/authorization/models/slim_role.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class SlimRole: + """The role included in the assignment.""" + + slug: str + """The slug of the assigned role.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "SlimRole": + """Deserialize from a dictionary.""" + try: + return cls( + slug=data["slug"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing SlimRole: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["slug"] = self.slug + return result diff --git a/src/workos/authorization/models/update_authorization_resource.py b/src/workos/authorization/models/update_authorization_resource.py new file mode 100644 index 00000000..a298a7d6 --- /dev/null +++ b/src/workos/authorization/models/update_authorization_resource.py @@ -0,0 +1,56 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class UpdateAuthorizationResource: + """Update Authorization Resource model.""" + + name: Optional[str] = None + """A display name for the resource.""" + description: Optional[str] = None + """An optional description of the resource.""" + parent_resource_id: Optional[str] = None + """The ID of the parent resource.""" + parent_resource_external_id: Optional[str] = None + """The external ID of the parent resource.""" + parent_resource_type_slug: Optional[str] = None + """The resource type slug of the parent resource.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "UpdateAuthorizationResource": + """Deserialize from a dictionary.""" + try: + return cls( + name=data.get("name"), + description=data.get("description"), + parent_resource_id=data.get("parent_resource_id"), + parent_resource_external_id=data.get("parent_resource_external_id"), + parent_resource_type_slug=data.get("parent_resource_type_slug"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UpdateAuthorizationResource: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + if self.name is not None: + result["name"] = self.name + if self.description is not None: + result["description"] = self.description + else: + result["description"] = None + if self.parent_resource_id is not None: + result["parent_resource_id"] = self.parent_resource_id + if self.parent_resource_external_id is not None: + result["parent_resource_external_id"] = self.parent_resource_external_id + if self.parent_resource_type_slug is not None: + result["parent_resource_type_slug"] = self.parent_resource_type_slug + return result diff --git a/src/workos/authorization/models/update_organization_role.py b/src/workos/authorization/models/update_organization_role.py new file mode 100644 index 00000000..654b53a0 --- /dev/null +++ b/src/workos/authorization/models/update_organization_role.py @@ -0,0 +1,41 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class UpdateOrganizationRole: + """Update Organization Role model.""" + + name: Optional[str] = None + """A descriptive name for the role.""" + description: Optional[str] = None + """An optional description of the role's purpose.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "UpdateOrganizationRole": + """Deserialize from a dictionary.""" + try: + return cls( + name=data.get("name"), + description=data.get("description"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UpdateOrganizationRole: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + if self.name is not None: + result["name"] = self.name + if self.description is not None: + result["description"] = self.description + else: + result["description"] = None + return result diff --git a/src/workos/authorization/models/update_role.py b/src/workos/authorization/models/update_role.py new file mode 100644 index 00000000..9c530ca4 --- /dev/null +++ b/src/workos/authorization/models/update_role.py @@ -0,0 +1,41 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class UpdateRole: + """Update Role model.""" + + name: Optional[str] = None + """A descriptive name for the role.""" + description: Optional[str] = None + """An optional description of the role.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "UpdateRole": + """Deserialize from a dictionary.""" + try: + return cls( + name=data.get("name"), + description=data.get("description"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UpdateRole: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + if self.name is not None: + result["name"] = self.name + if self.description is not None: + result["description"] = self.description + else: + result["description"] = None + return result diff --git a/src/workos/authorization/models/user_organization_membership_base_list_data.py b/src/workos/authorization/models/user_organization_membership_base_list_data.py new file mode 100644 index 00000000..15f5d92e --- /dev/null +++ b/src/workos/authorization/models/user_organization_membership_base_list_data.py @@ -0,0 +1,83 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException +from workos.common.models import UserOrganizationMembershipBaseListDataStatus + + +@dataclass(slots=True) +class UserOrganizationMembershipBaseListData: + """User Organization Membership Base List Data model.""" + + object: Literal["organization_membership"] + """Distinguishes the organization membership object.""" + id: str + """The unique ID of the organization membership.""" + user_id: str + """The ID of the user.""" + organization_id: str + """The ID of the organization which the user belongs to.""" + status: "UserOrganizationMembershipBaseListDataStatus" + """The status of the organization membership. One of `active`, `inactive`, or `pending`.""" + directory_managed: bool + """Whether this organization membership is managed by a directory sync connection.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + organization_name: Optional[str] = None + """The name of the organization which the user belongs to.""" + custom_attributes: Optional[Dict[str, Any]] = None + """An object containing IdP-sourced attributes from the linked [Directory User](https://workos.com/docs/reference/directory-sync/directory-user) or [SSO Profile](https://workos.com/docs/reference/sso/profile). Directory User attributes take precedence when both are linked.""" + + @classmethod + def from_dict( + cls, data: Dict[str, Any] + ) -> "UserOrganizationMembershipBaseListData": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + user_id=data["user_id"], + organization_id=data["organization_id"], + status=UserOrganizationMembershipBaseListDataStatus(data["status"]), + directory_managed=data["directory_managed"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + organization_name=data.get("organization_name"), + custom_attributes=data.get("custom_attributes"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UserOrganizationMembershipBaseListData: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["user_id"] = self.user_id + result["organization_id"] = self.organization_id + result["status"] = self.status + result["directory_managed"] = self.directory_managed + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + if self.organization_name is not None: + result["organization_name"] = self.organization_name + if self.custom_attributes is not None: + result["custom_attributes"] = self.custom_attributes + return result diff --git a/src/workos/client.py b/src/workos/client.py new file mode 100644 index 00000000..34e026d6 --- /dev/null +++ b/src/workos/client.py @@ -0,0 +1,8 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._client import WorkOSClient, WorkOS + +SyncClient = WorkOSClient +Client = WorkOSClient + +__all__ = ["SyncClient", "Client", "WorkOSClient", "WorkOS"] diff --git a/src/workos/common/__init__.py b/src/workos/common/__init__.py new file mode 100644 index 00000000..beae7f53 --- /dev/null +++ b/src/workos/common/__init__.py @@ -0,0 +1,81 @@ +# This file is auto-generated by oagen. Do not edit. + +from .models import ( + AuditLogConfigurationLogStreamState as AuditLogConfigurationLogStreamState, +) +from .models import ( + AuditLogConfigurationLogStreamType as AuditLogConfigurationLogStreamType, +) +from .models import AuditLogConfigurationState as AuditLogConfigurationState +from .models import AuditLogExportJsonState as AuditLogExportJsonState +from .models import AuditLogExportState as AuditLogExportState +from .models import AuditLogStreamState as AuditLogStreamState +from .models import AuditLogTrailState as AuditLogTrailState +from .models import AuthMethodType as AuthMethodType +from .models import ( + AuthenticateResponseAuthenticationMethod as AuthenticateResponseAuthenticationMethod, +) +from .models import AuthenticationFactorEnrolledType as AuthenticationFactorEnrolledType +from .models import AuthenticationFactorType as AuthenticationFactorType +from .models import ( + AuthenticationFactorsCreateRequestType as AuthenticationFactorsCreateRequestType, +) +from .models import ConnectedAccountState as ConnectedAccountState +from .models import ConnectionState as ConnectionState +from .models import ConnectionStatus as ConnectionStatus +from .models import ConnectionType as ConnectionType +from .models import CreateUserDtoPasswordHashType as CreateUserDtoPasswordHashType +from .models import CreateUserInviteOptionsDtoLocale as CreateUserInviteOptionsDtoLocale +from .models import CreateWebhookEndpointDtoEvents as CreateWebhookEndpointDtoEvents +from .models import ( + DataIntegrationsListResponseDataConnectedAccountState as DataIntegrationsListResponseDataConnectedAccountState, +) +from .models import ( + DataIntegrationsListResponseDataOwnership as DataIntegrationsListResponseDataOwnership, +) +from .models import DirectoryState as DirectoryState +from .models import DirectoryType as DirectoryType +from .models import DirectoryUserWithGroupsState as DirectoryUserWithGroupsState +from .models import GenerateLinkDtoIntent as GenerateLinkDtoIntent +from .models import InvitationState as InvitationState +from .models import ListDataType as ListDataType +from .models import OrganizationDomainDataDtoState as OrganizationDomainDataDtoState +from .models import ( + OrganizationDomainStandAloneState as OrganizationDomainStandAloneState, +) +from .models import ( + OrganizationDomainStandAloneVerificationStrategy as OrganizationDomainStandAloneVerificationStrategy, +) +from .models import OrganizationDomainState as OrganizationDomainState +from .models import ( + OrganizationDomainVerificationStrategy as OrganizationDomainVerificationStrategy, +) +from .models import OrganizationMembershipStatus as OrganizationMembershipStatus +from .models import PasswordHashType as PasswordHashType +from .models import ProfileConnectionType as ProfileConnectionType +from .models import ( + RadarStandaloneAssessRequestAction as RadarStandaloneAssessRequestAction, +) +from .models import ( + RadarStandaloneAssessRequestAuthMethod as RadarStandaloneAssessRequestAuthMethod, +) +from .models import ( + RadarStandaloneResponseBlocklistType as RadarStandaloneResponseBlocklistType, +) +from .models import RadarStandaloneResponseControl as RadarStandaloneResponseControl +from .models import RadarStandaloneResponseVerdict as RadarStandaloneResponseVerdict +from .models import ResendUserInviteOptionsDtoLocale as ResendUserInviteOptionsDtoLocale +from .models import RoleType as RoleType +from .models import UpdateUserDtoPasswordHashType as UpdateUserDtoPasswordHashType +from .models import UpdateWebhookEndpointDtoEvents as UpdateWebhookEndpointDtoEvents +from .models import UpdateWebhookEndpointDtoStatus as UpdateWebhookEndpointDtoStatus +from .models import UserIdentitiesGetItemProvider as UserIdentitiesGetItemProvider +from .models import UserInviteState as UserInviteState +from .models import ( + UserOrganizationMembershipBaseListDataStatus as UserOrganizationMembershipBaseListDataStatus, +) +from .models import UserOrganizationMembershipStatus as UserOrganizationMembershipStatus +from .models import UserSessionsAuthMethod as UserSessionsAuthMethod +from .models import UserSessionsStatus as UserSessionsStatus +from .models import WebhookEndpointJsonStatus as WebhookEndpointJsonStatus +from .models import WidgetSessionTokenDtoScopes as WidgetSessionTokenDtoScopes diff --git a/src/workos/common/models/__init__.py b/src/workos/common/models/__init__.py new file mode 100644 index 00000000..761a821d --- /dev/null +++ b/src/workos/common/models/__init__.py @@ -0,0 +1,123 @@ +# This file is auto-generated by oagen. Do not edit. + +from .audit_log_configuration_log_stream_state import ( + AuditLogConfigurationLogStreamState as AuditLogConfigurationLogStreamState, +) +from .audit_log_configuration_log_stream_type import ( + AuditLogConfigurationLogStreamType as AuditLogConfigurationLogStreamType, +) +from .audit_log_configuration_state import ( + AuditLogConfigurationState as AuditLogConfigurationState, +) +from .audit_log_export_json_state import ( + AuditLogExportJsonState as AuditLogExportJsonState, +) +from .audit_log_export_state import AuditLogExportState as AuditLogExportState +from .audit_log_stream_state import AuditLogStreamState as AuditLogStreamState +from .audit_log_trail_state import AuditLogTrailState as AuditLogTrailState +from .auth_method_type import AuthMethodType as AuthMethodType +from .authenticate_response_authentication_method import ( + AuthenticateResponseAuthenticationMethod as AuthenticateResponseAuthenticationMethod, +) +from .authentication_factor_enrolled_type import ( + AuthenticationFactorEnrolledType as AuthenticationFactorEnrolledType, +) +from .authentication_factor_type import ( + AuthenticationFactorType as AuthenticationFactorType, +) +from .authentication_factors_create_request_type import ( + AuthenticationFactorsCreateRequestType as AuthenticationFactorsCreateRequestType, +) +from .connected_account_state import ConnectedAccountState as ConnectedAccountState +from .connection_state import ConnectionState as ConnectionState +from .connection_status import ConnectionStatus as ConnectionStatus +from .connection_type import ConnectionType as ConnectionType +from .create_user_dto_password_hash_type import ( + CreateUserDtoPasswordHashType as CreateUserDtoPasswordHashType, +) +from .create_user_invite_options_dto_locale import ( + CreateUserInviteOptionsDtoLocale as CreateUserInviteOptionsDtoLocale, +) +from .create_webhook_endpoint_dto_events import ( + CreateWebhookEndpointDtoEvents as CreateWebhookEndpointDtoEvents, +) +from .data_integrations_list_response_data_connected_account_state import ( + DataIntegrationsListResponseDataConnectedAccountState as DataIntegrationsListResponseDataConnectedAccountState, +) +from .data_integrations_list_response_data_ownership import ( + DataIntegrationsListResponseDataOwnership as DataIntegrationsListResponseDataOwnership, +) +from .directory_state import DirectoryState as DirectoryState +from .directory_type import DirectoryType as DirectoryType +from .directory_user_with_groups_state import ( + DirectoryUserWithGroupsState as DirectoryUserWithGroupsState, +) +from .generate_link_dto_intent import GenerateLinkDtoIntent as GenerateLinkDtoIntent +from .invitation_state import InvitationState as InvitationState +from .list_data_type import ListDataType as ListDataType +from .organization_domain_data_dto_state import ( + OrganizationDomainDataDtoState as OrganizationDomainDataDtoState, +) +from .organization_domain_stand_alone_state import ( + OrganizationDomainStandAloneState as OrganizationDomainStandAloneState, +) +from .organization_domain_stand_alone_verification_strategy import ( + OrganizationDomainStandAloneVerificationStrategy as OrganizationDomainStandAloneVerificationStrategy, +) +from .organization_domain_state import ( + OrganizationDomainState as OrganizationDomainState, +) +from .organization_domain_verification_strategy import ( + OrganizationDomainVerificationStrategy as OrganizationDomainVerificationStrategy, +) +from .organization_membership_status import ( + OrganizationMembershipStatus as OrganizationMembershipStatus, +) +from .password_hash_type import PasswordHashType as PasswordHashType +from .profile_connection_type import ProfileConnectionType as ProfileConnectionType +from .radar_standalone_assess_request_action import ( + RadarStandaloneAssessRequestAction as RadarStandaloneAssessRequestAction, +) +from .radar_standalone_assess_request_auth_method import ( + RadarStandaloneAssessRequestAuthMethod as RadarStandaloneAssessRequestAuthMethod, +) +from .radar_standalone_response_blocklist_type import ( + RadarStandaloneResponseBlocklistType as RadarStandaloneResponseBlocklistType, +) +from .radar_standalone_response_control import ( + RadarStandaloneResponseControl as RadarStandaloneResponseControl, +) +from .radar_standalone_response_verdict import ( + RadarStandaloneResponseVerdict as RadarStandaloneResponseVerdict, +) +from .resend_user_invite_options_dto_locale import ( + ResendUserInviteOptionsDtoLocale as ResendUserInviteOptionsDtoLocale, +) +from .role_type import RoleType as RoleType +from .update_user_dto_password_hash_type import ( + UpdateUserDtoPasswordHashType as UpdateUserDtoPasswordHashType, +) +from .update_webhook_endpoint_dto_events import ( + UpdateWebhookEndpointDtoEvents as UpdateWebhookEndpointDtoEvents, +) +from .update_webhook_endpoint_dto_status import ( + UpdateWebhookEndpointDtoStatus as UpdateWebhookEndpointDtoStatus, +) +from .user_identities_get_item_provider import ( + UserIdentitiesGetItemProvider as UserIdentitiesGetItemProvider, +) +from .user_invite_state import UserInviteState as UserInviteState +from .user_organization_membership_base_list_data_status import ( + UserOrganizationMembershipBaseListDataStatus as UserOrganizationMembershipBaseListDataStatus, +) +from .user_organization_membership_status import ( + UserOrganizationMembershipStatus as UserOrganizationMembershipStatus, +) +from .user_sessions_auth_method import UserSessionsAuthMethod as UserSessionsAuthMethod +from .user_sessions_status import UserSessionsStatus as UserSessionsStatus +from .webhook_endpoint_json_status import ( + WebhookEndpointJsonStatus as WebhookEndpointJsonStatus, +) +from .widget_session_token_dto_scopes import ( + WidgetSessionTokenDtoScopes as WidgetSessionTokenDtoScopes, +) diff --git a/src/workos/common/models/audit_log_configuration_log_stream_state.py b/src/workos/common/models/audit_log_configuration_log_stream_state.py new file mode 100644 index 00000000..d076b31f --- /dev/null +++ b/src/workos/common/models/audit_log_configuration_log_stream_state.py @@ -0,0 +1,34 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of audit log configuration log stream state values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class AuditLogConfigurationLogStreamState(str, Enum): + """Known values for AuditLogConfigurationLogStreamState.""" + + ACTIVE = "active" + INACTIVE = "inactive" + ERROR = "error" + INVALID = "invalid" + + @classmethod + def _missing_( + cls, value: object + ) -> Optional["AuditLogConfigurationLogStreamState"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +AuditLogConfigurationLogStreamStateLiteral: TypeAlias = Literal[ + "active", "inactive", "error", "invalid" +] diff --git a/src/workos/common/models/audit_log_configuration_log_stream_type.py b/src/workos/common/models/audit_log_configuration_log_stream_type.py new file mode 100644 index 00000000..50524faf --- /dev/null +++ b/src/workos/common/models/audit_log_configuration_log_stream_type.py @@ -0,0 +1,34 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of audit log configuration log stream type values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class AuditLogConfigurationLogStreamType(str, Enum): + """Known values for AuditLogConfigurationLogStreamType.""" + + AZURE_SENTINEL = "AzureSentinel" + DATADOG = "Datadog" + GENERIC_HTTPS = "GenericHttps" + GOOGLE_CLOUD_STORAGE = "GoogleCloudStorage" + S_3 = "S3" + SPLUNK = "Splunk" + + @classmethod + def _missing_(cls, value: object) -> Optional["AuditLogConfigurationLogStreamType"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +AuditLogConfigurationLogStreamTypeLiteral: TypeAlias = Literal[ + "AzureSentinel", "Datadog", "GenericHttps", "GoogleCloudStorage", "S3", "Splunk" +] diff --git a/src/workos/common/models/audit_log_configuration_state.py b/src/workos/common/models/audit_log_configuration_state.py new file mode 100644 index 00000000..6bea7264 --- /dev/null +++ b/src/workos/common/models/audit_log_configuration_state.py @@ -0,0 +1,29 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of audit log configuration state values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class AuditLogConfigurationState(str, Enum): + """Known values for AuditLogConfigurationState.""" + + ACTIVE = "active" + INACTIVE = "inactive" + DISABLED = "disabled" + + @classmethod + def _missing_(cls, value: object) -> Optional["AuditLogConfigurationState"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +AuditLogConfigurationStateLiteral: TypeAlias = Literal["active", "inactive", "disabled"] diff --git a/src/workos/common/models/audit_log_export_json_state.py b/src/workos/common/models/audit_log_export_json_state.py new file mode 100644 index 00000000..0a405506 --- /dev/null +++ b/src/workos/common/models/audit_log_export_json_state.py @@ -0,0 +1,29 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of audit log export json state values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class AuditLogExportJsonState(str, Enum): + """Known values for AuditLogExportJsonState.""" + + PENDING = "pending" + READY = "ready" + ERROR = "error" + + @classmethod + def _missing_(cls, value: object) -> Optional["AuditLogExportJsonState"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +AuditLogExportJsonStateLiteral: TypeAlias = Literal["pending", "ready", "error"] diff --git a/src/workos/common/models/audit_log_export_state.py b/src/workos/common/models/audit_log_export_state.py new file mode 100644 index 00000000..9b959e89 --- /dev/null +++ b/src/workos/common/models/audit_log_export_state.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .audit_log_export_json_state import AuditLogExportJsonState + +AuditLogExportState = AuditLogExportJsonState +__all__ = ["AuditLogExportState"] diff --git a/src/workos/common/models/audit_log_stream_state.py b/src/workos/common/models/audit_log_stream_state.py new file mode 100644 index 00000000..fe2e4e54 --- /dev/null +++ b/src/workos/common/models/audit_log_stream_state.py @@ -0,0 +1,8 @@ +# This file is auto-generated by oagen. Do not edit. + +from .audit_log_configuration_log_stream_state import ( + AuditLogConfigurationLogStreamState, +) + +AuditLogStreamState = AuditLogConfigurationLogStreamState +__all__ = ["AuditLogStreamState"] diff --git a/src/workos/common/models/audit_log_trail_state.py b/src/workos/common/models/audit_log_trail_state.py new file mode 100644 index 00000000..b902eee0 --- /dev/null +++ b/src/workos/common/models/audit_log_trail_state.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .audit_log_configuration_state import AuditLogConfigurationState + +AuditLogTrailState = AuditLogConfigurationState +__all__ = ["AuditLogTrailState"] diff --git a/src/workos/common/models/auth_method_type.py b/src/workos/common/models/auth_method_type.py new file mode 100644 index 00000000..655b06c5 --- /dev/null +++ b/src/workos/common/models/auth_method_type.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .user_sessions_auth_method import UserSessionsAuthMethod + +AuthMethodType = UserSessionsAuthMethod +__all__ = ["AuthMethodType"] diff --git a/src/workos/common/models/authenticate_response_authentication_method.py b/src/workos/common/models/authenticate_response_authentication_method.py new file mode 100644 index 00000000..28249720 --- /dev/null +++ b/src/workos/common/models/authenticate_response_authentication_method.py @@ -0,0 +1,73 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of authenticate response authentication method values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class AuthenticateResponseAuthenticationMethod(str, Enum): + """Known values for AuthenticateResponseAuthenticationMethod.""" + + SSO = "SSO" + PASSWORD = "Password" + PASSKEY = "Passkey" + APPLE_OAUTH = "AppleOAuth" + BITBUCKET_OAUTH = "BitbucketOAuth" + CROSS_APP_AUTH = "CrossAppAuth" + DISCORD_OAUTH = "DiscordOAuth" + EXTERNAL_AUTH = "ExternalAuth" + GIT_HUB_OAUTH = "GitHubOAuth" + GIT_LAB_OAUTH = "GitLabOAuth" + GOOGLE_OAUTH = "GoogleOAuth" + INTUIT_OAUTH = "IntuitOAuth" + LINKED_IN_OAUTH = "LinkedInOAuth" + MICROSOFT_OAUTH = "MicrosoftOAuth" + SALESFORCE_OAUTH = "SalesforceOAuth" + SLACK_OAUTH = "SlackOAuth" + VERCEL_MARKETPLACE_OAUTH = "VercelMarketplaceOAuth" + VERCEL_OAUTH = "VercelOAuth" + XERO_OAUTH = "XeroOAuth" + MAGIC_AUTH = "MagicAuth" + IMPERSONATION = "Impersonation" + MIGRATED_SESSION = "MigratedSession" + + @classmethod + def _missing_( + cls, value: object + ) -> Optional["AuthenticateResponseAuthenticationMethod"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +AuthenticateResponseAuthenticationMethodLiteral: TypeAlias = Literal[ + "SSO", + "Password", + "Passkey", + "AppleOAuth", + "BitbucketOAuth", + "CrossAppAuth", + "DiscordOAuth", + "ExternalAuth", + "GitHubOAuth", + "GitLabOAuth", + "GoogleOAuth", + "IntuitOAuth", + "LinkedInOAuth", + "MicrosoftOAuth", + "SalesforceOAuth", + "SlackOAuth", + "VercelMarketplaceOAuth", + "VercelOAuth", + "XeroOAuth", + "MagicAuth", + "Impersonation", + "MigratedSession", +] diff --git a/src/workos/common/models/authentication_factor_enrolled_type.py b/src/workos/common/models/authentication_factor_enrolled_type.py new file mode 100644 index 00000000..4a1c401c --- /dev/null +++ b/src/workos/common/models/authentication_factor_enrolled_type.py @@ -0,0 +1,32 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of authentication factor enrolled type values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class AuthenticationFactorEnrolledType(str, Enum): + """Known values for AuthenticationFactorEnrolledType.""" + + GENERIC_OTP = "generic_otp" + SMS = "sms" + TOTP = "totp" + WEBAUTHN = "webauthn" + + @classmethod + def _missing_(cls, value: object) -> Optional["AuthenticationFactorEnrolledType"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +AuthenticationFactorEnrolledTypeLiteral: TypeAlias = Literal[ + "generic_otp", "sms", "totp", "webauthn" +] diff --git a/src/workos/common/models/authentication_factor_type.py b/src/workos/common/models/authentication_factor_type.py new file mode 100644 index 00000000..b1124813 --- /dev/null +++ b/src/workos/common/models/authentication_factor_type.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .authentication_factor_enrolled_type import AuthenticationFactorEnrolledType + +AuthenticationFactorType = AuthenticationFactorEnrolledType +__all__ = ["AuthenticationFactorType"] diff --git a/src/workos/common/models/authentication_factors_create_request_type.py b/src/workos/common/models/authentication_factors_create_request_type.py new file mode 100644 index 00000000..1993b6ad --- /dev/null +++ b/src/workos/common/models/authentication_factors_create_request_type.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of authentication factors create request type values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class AuthenticationFactorsCreateRequestType(str, Enum): + """Known values for AuthenticationFactorsCreateRequestType.""" + + GENERIC_OTP = "generic_otp" + SMS = "sms" + TOTP = "totp" + + @classmethod + def _missing_( + cls, value: object + ) -> Optional["AuthenticationFactorsCreateRequestType"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +AuthenticationFactorsCreateRequestTypeLiteral: TypeAlias = Literal[ + "generic_otp", "sms", "totp" +] diff --git a/src/workos/common/models/connected_account_state.py b/src/workos/common/models/connected_account_state.py new file mode 100644 index 00000000..39890ce2 --- /dev/null +++ b/src/workos/common/models/connected_account_state.py @@ -0,0 +1,31 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of connected account state values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class ConnectedAccountState(str, Enum): + """Known values for ConnectedAccountState.""" + + CONNECTED = "connected" + NEEDS_REAUTHORIZATION = "needs_reauthorization" + DISCONNECTED = "disconnected" + + @classmethod + def _missing_(cls, value: object) -> Optional["ConnectedAccountState"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +ConnectedAccountStateLiteral: TypeAlias = Literal[ + "connected", "needs_reauthorization", "disconnected" +] diff --git a/src/workos/common/models/connection_state.py b/src/workos/common/models/connection_state.py new file mode 100644 index 00000000..e10bbd7b --- /dev/null +++ b/src/workos/common/models/connection_state.py @@ -0,0 +1,34 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of connection state values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class ConnectionState(str, Enum): + """Known values for ConnectionState.""" + + REQUIRES_TYPE = "requires_type" + DRAFT = "draft" + ACTIVE = "active" + VALIDATING = "validating" + INACTIVE = "inactive" + DELETING = "deleting" + + @classmethod + def _missing_(cls, value: object) -> Optional["ConnectionState"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +ConnectionStateLiteral: TypeAlias = Literal[ + "requires_type", "draft", "active", "validating", "inactive", "deleting" +] diff --git a/src/workos/common/models/connection_status.py b/src/workos/common/models/connection_status.py new file mode 100644 index 00000000..408159c4 --- /dev/null +++ b/src/workos/common/models/connection_status.py @@ -0,0 +1,28 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of connection status values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class ConnectionStatus(str, Enum): + """Known values for ConnectionStatus.""" + + LINKED = "linked" + UNLINKED = "unlinked" + + @classmethod + def _missing_(cls, value: object) -> Optional["ConnectionStatus"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +ConnectionStatusLiteral: TypeAlias = Literal["linked", "unlinked"] diff --git a/src/workos/common/models/connection_type.py b/src/workos/common/models/connection_type.py new file mode 100644 index 00000000..3e59e1bb --- /dev/null +++ b/src/workos/common/models/connection_type.py @@ -0,0 +1,129 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of connection type values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class ConnectionType(str, Enum): + """Known values for ConnectionType.""" + + PENDING = "Pending" + ADFSSAML = "ADFSSAML" + ADP_OIDC = "AdpOidc" + APPLE_OAUTH = "AppleOAuth" + AUTH_0_MIGRATION = "Auth0Migration" + AUTH_0_SAML = "Auth0SAML" + AZURE_SAML = "AzureSAML" + BITBUCKET_OAUTH = "BitbucketOAuth" + CAS_SAML = "CasSAML" + CLASS_LINK_SAML = "ClassLinkSAML" + CLEVER_OIDC = "CleverOIDC" + CLOUDFLARE_SAML = "CloudflareSAML" + CYBER_ARK_SAML = "CyberArkSAML" + DISCORD_OAUTH = "DiscordOAuth" + DUO_SAML = "DuoSAML" + ENTRA_ID_OIDC = "EntraIdOIDC" + GENERIC_OIDC = "GenericOIDC" + GENERIC_SAML = "GenericSAML" + GIT_HUB_OAUTH = "GitHubOAuth" + GIT_LAB_OAUTH = "GitLabOAuth" + GOOGLE_OAUTH = "GoogleOAuth" + GOOGLE_OIDC = "GoogleOIDC" + GOOGLE_SAML = "GoogleSAML" + INTUIT_OAUTH = "IntuitOAuth" + JUMP_CLOUD_SAML = "JumpCloudSAML" + KEYCLOAK_SAML = "KeycloakSAML" + LAST_PASS_SAML = "LastPassSAML" + LINKED_IN_OAUTH = "LinkedInOAuth" + LOGIN_GOV_OIDC = "LoginGovOidc" + MAGIC_LINK = "MagicLink" + MICROSOFT_OAUTH = "MicrosoftOAuth" + MINI_ORANGE_SAML = "MiniOrangeSAML" + NET_IQ_SAML = "NetIqSAML" + OKTA_OIDC = "OktaOIDC" + OKTA_SAML = "OktaSAML" + ONE_LOGIN_SAML = "OneLoginSAML" + ORACLE_SAML = "OracleSAML" + PING_FEDERATE_SAML = "PingFederateSAML" + PING_ONE_SAML = "PingOneSAML" + RIPPLING_SAML = "RipplingSAML" + SALESFORCE_SAML = "SalesforceSAML" + SHIBBOLETH_GENERIC_SAML = "ShibbolethGenericSAML" + SHIBBOLETH_SAML = "ShibbolethSAML" + SIMPLE_SAML_PHP_SAML = "SimpleSamlPhpSAML" + SALESFORCE_OAUTH = "SalesforceOAuth" + SLACK_OAUTH = "SlackOAuth" + TEST_IDP = "TestIdp" + VERCEL_MARKETPLACE_OAUTH = "VercelMarketplaceOAuth" + VERCEL_OAUTH = "VercelOAuth" + V_MWARE_SAML = "VMwareSAML" + XERO_OAUTH = "XeroOAuth" + + @classmethod + def _missing_(cls, value: object) -> Optional["ConnectionType"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +ConnectionTypeLiteral: TypeAlias = Literal[ + "Pending", + "ADFSSAML", + "AdpOidc", + "AppleOAuth", + "Auth0Migration", + "Auth0SAML", + "AzureSAML", + "BitbucketOAuth", + "CasSAML", + "ClassLinkSAML", + "CleverOIDC", + "CloudflareSAML", + "CyberArkSAML", + "DiscordOAuth", + "DuoSAML", + "EntraIdOIDC", + "GenericOIDC", + "GenericSAML", + "GitHubOAuth", + "GitLabOAuth", + "GoogleOAuth", + "GoogleOIDC", + "GoogleSAML", + "IntuitOAuth", + "JumpCloudSAML", + "KeycloakSAML", + "LastPassSAML", + "LinkedInOAuth", + "LoginGovOidc", + "MagicLink", + "MicrosoftOAuth", + "MiniOrangeSAML", + "NetIqSAML", + "OktaOIDC", + "OktaSAML", + "OneLoginSAML", + "OracleSAML", + "PingFederateSAML", + "PingOneSAML", + "RipplingSAML", + "SalesforceSAML", + "ShibbolethGenericSAML", + "ShibbolethSAML", + "SimpleSamlPhpSAML", + "SalesforceOAuth", + "SlackOAuth", + "TestIdp", + "VercelMarketplaceOAuth", + "VercelOAuth", + "VMwareSAML", + "XeroOAuth", +] diff --git a/src/workos/common/models/create_user_dto_password_hash_type.py b/src/workos/common/models/create_user_dto_password_hash_type.py new file mode 100644 index 00000000..1f507022 --- /dev/null +++ b/src/workos/common/models/create_user_dto_password_hash_type.py @@ -0,0 +1,34 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of create user dto password hash type values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class CreateUserDtoPasswordHashType(str, Enum): + """Known values for CreateUserDtoPasswordHashType.""" + + BCRYPT = "bcrypt" + FIREBASE_SCRYPT = "firebase-scrypt" + SSHA = "ssha" + SCRYPT = "scrypt" + PBKDF_2 = "pbkdf2" + ARGON_2 = "argon2" + + @classmethod + def _missing_(cls, value: object) -> Optional["CreateUserDtoPasswordHashType"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +CreateUserDtoPasswordHashTypeLiteral: TypeAlias = Literal[ + "bcrypt", "firebase-scrypt", "ssha", "scrypt", "pbkdf2", "argon2" +] diff --git a/src/workos/common/models/create_user_invite_options_dto_locale.py b/src/workos/common/models/create_user_invite_options_dto_locale.py new file mode 100644 index 00000000..6b124b30 --- /dev/null +++ b/src/workos/common/models/create_user_invite_options_dto_locale.py @@ -0,0 +1,207 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of create user invite options dto locale values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class CreateUserInviteOptionsDtoLocale(str, Enum): + """Known values for CreateUserInviteOptionsDtoLocale.""" + + AF = "af" + AM = "am" + AR = "ar" + BG = "bg" + BN = "bn" + BS = "bs" + CA = "ca" + CS = "cs" + DA = "da" + DE = "de" + DE_DE = "de-DE" + EL = "el" + EN = "en" + EN_AU = "en-AU" + EN_CA = "en-CA" + EN_GB = "en-GB" + EN_US = "en-US" + ES = "es" + ES_419 = "es-419" + ES_ES = "es-ES" + ES_US = "es-US" + ET = "et" + FA = "fa" + FI = "fi" + FIL = "fil" + FR = "fr" + FR_BE = "fr-BE" + FR_CA = "fr-CA" + FR_FR = "fr-FR" + FY = "fy" + GL = "gl" + GU = "gu" + HA = "ha" + HE = "he" + HI = "hi" + HR = "hr" + HU = "hu" + HY = "hy" + ID = "id" + IS = "is" + IT = "it" + IT_IT = "it-IT" + JA = "ja" + JV = "jv" + KA = "ka" + KK = "kk" + KM = "km" + KN = "kn" + KO = "ko" + LT = "lt" + LV = "lv" + MK = "mk" + ML = "ml" + MN = "mn" + MR = "mr" + MS = "ms" + MY = "my" + NB = "nb" + NE = "ne" + NL = "nl" + NL_BE = "nl-BE" + NL_NL = "nl-NL" + NN = "nn" + NO = "no" + PA = "pa" + PL = "pl" + PT = "pt" + PT_BR = "pt-BR" + PT_PT = "pt-PT" + RO = "ro" + RU = "ru" + SK = "sk" + SL = "sl" + SQ = "sq" + SR = "sr" + SV = "sv" + SW = "sw" + TA = "ta" + TE = "te" + TH = "th" + TR = "tr" + UK = "uk" + UR = "ur" + UZ = "uz" + VI = "vi" + ZH = "zh" + ZH_CN = "zh-CN" + ZH_HK = "zh-HK" + ZH_TW = "zh-TW" + ZU = "zu" + + @classmethod + def _missing_(cls, value: object) -> Optional["CreateUserInviteOptionsDtoLocale"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +CreateUserInviteOptionsDtoLocaleLiteral: TypeAlias = Literal[ + "af", + "am", + "ar", + "bg", + "bn", + "bs", + "ca", + "cs", + "da", + "de", + "de-DE", + "el", + "en", + "en-AU", + "en-CA", + "en-GB", + "en-US", + "es", + "es-419", + "es-ES", + "es-US", + "et", + "fa", + "fi", + "fil", + "fr", + "fr-BE", + "fr-CA", + "fr-FR", + "fy", + "gl", + "gu", + "ha", + "he", + "hi", + "hr", + "hu", + "hy", + "id", + "is", + "it", + "it-IT", + "ja", + "jv", + "ka", + "kk", + "km", + "kn", + "ko", + "lt", + "lv", + "mk", + "ml", + "mn", + "mr", + "ms", + "my", + "nb", + "ne", + "nl", + "nl-BE", + "nl-NL", + "nn", + "no", + "pa", + "pl", + "pt", + "pt-BR", + "pt-PT", + "ro", + "ru", + "sk", + "sl", + "sq", + "sr", + "sv", + "sw", + "ta", + "te", + "th", + "tr", + "uk", + "ur", + "uz", + "vi", + "zh", + "zh-CN", + "zh-HK", + "zh-TW", + "zu", +] diff --git a/src/workos/common/models/create_webhook_endpoint_dto_events.py b/src/workos/common/models/create_webhook_endpoint_dto_events.py new file mode 100644 index 00000000..3f62f5e1 --- /dev/null +++ b/src/workos/common/models/create_webhook_endpoint_dto_events.py @@ -0,0 +1,169 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of create webhook endpoint dto events values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class CreateWebhookEndpointDtoEvents(str, Enum): + """Known values for CreateWebhookEndpointDtoEvents.""" + + AUTHENTICATION_EMAIL_VERIFICATION_SUCCEEDED = ( + "authentication.email_verification_succeeded" + ) + AUTHENTICATION_MAGIC_AUTH_FAILED = "authentication.magic_auth_failed" + AUTHENTICATION_MAGIC_AUTH_SUCCEEDED = "authentication.magic_auth_succeeded" + AUTHENTICATION_MFA_SUCCEEDED = "authentication.mfa_succeeded" + AUTHENTICATION_OAUTH_FAILED = "authentication.oauth_failed" + AUTHENTICATION_OAUTH_SUCCEEDED = "authentication.oauth_succeeded" + AUTHENTICATION_PASSWORD_FAILED = "authentication.password_failed" + AUTHENTICATION_PASSWORD_SUCCEEDED = "authentication.password_succeeded" + AUTHENTICATION_PASSKEY_FAILED = "authentication.passkey_failed" + AUTHENTICATION_PASSKEY_SUCCEEDED = "authentication.passkey_succeeded" + AUTHENTICATION_SSO_FAILED = "authentication.sso_failed" + AUTHENTICATION_SSO_STARTED = "authentication.sso_started" + AUTHENTICATION_SSO_SUCCEEDED = "authentication.sso_succeeded" + AUTHENTICATION_SSO_TIMED_OUT = "authentication.sso_timed_out" + AUTHENTICATION_RADAR_RISK_DETECTED = "authentication.radar_risk_detected" + API_KEY_CREATED = "api_key.created" + API_KEY_REVOKED = "api_key.revoked" + CONNECTION_ACTIVATED = "connection.activated" + CONNECTION_DEACTIVATED = "connection.deactivated" + CONNECTION_SAML_CERTIFICATE_RENEWAL_REQUIRED = ( + "connection.saml_certificate_renewal_required" + ) + CONNECTION_SAML_CERTIFICATE_RENEWED = "connection.saml_certificate_renewed" + CONNECTION_DELETED = "connection.deleted" + DSYNC_ACTIVATED = "dsync.activated" + DSYNC_DELETED = "dsync.deleted" + DSYNC_GROUP_CREATED = "dsync.group.created" + DSYNC_GROUP_DELETED = "dsync.group.deleted" + DSYNC_GROUP_UPDATED = "dsync.group.updated" + DSYNC_GROUP_USER_ADDED = "dsync.group.user_added" + DSYNC_GROUP_USER_REMOVED = "dsync.group.user_removed" + DSYNC_USER_CREATED = "dsync.user.created" + DSYNC_USER_DELETED = "dsync.user.deleted" + DSYNC_USER_UPDATED = "dsync.user.updated" + EMAIL_VERIFICATION_CREATED = "email_verification.created" + FLAG_CREATED = "flag.created" + FLAG_DELETED = "flag.deleted" + FLAG_UPDATED = "flag.updated" + FLAG_RULE_UPDATED = "flag.rule_updated" + INVITATION_ACCEPTED = "invitation.accepted" + INVITATION_CREATED = "invitation.created" + INVITATION_RESENT = "invitation.resent" + INVITATION_REVOKED = "invitation.revoked" + MAGIC_AUTH_CREATED = "magic_auth.created" + ORGANIZATION_CREATED = "organization.created" + ORGANIZATION_DELETED = "organization.deleted" + ORGANIZATION_UPDATED = "organization.updated" + ORGANIZATION_DOMAIN_CREATED = "organization_domain.created" + ORGANIZATION_DOMAIN_DELETED = "organization_domain.deleted" + ORGANIZATION_DOMAIN_UPDATED = "organization_domain.updated" + ORGANIZATION_DOMAIN_VERIFIED = "organization_domain.verified" + ORGANIZATION_DOMAIN_VERIFICATION_FAILED = "organization_domain.verification_failed" + PASSWORD_RESET_CREATED = "password_reset.created" + PASSWORD_RESET_SUCCEEDED = "password_reset.succeeded" + USER_CREATED = "user.created" + USER_UPDATED = "user.updated" + USER_DELETED = "user.deleted" + ORGANIZATION_MEMBERSHIP_CREATED = "organization_membership.created" + ORGANIZATION_MEMBERSHIP_DELETED = "organization_membership.deleted" + ORGANIZATION_MEMBERSHIP_UPDATED = "organization_membership.updated" + ROLE_CREATED = "role.created" + ROLE_DELETED = "role.deleted" + ROLE_UPDATED = "role.updated" + ORGANIZATION_ROLE_CREATED = "organization_role.created" + ORGANIZATION_ROLE_DELETED = "organization_role.deleted" + ORGANIZATION_ROLE_UPDATED = "organization_role.updated" + PERMISSION_CREATED = "permission.created" + PERMISSION_DELETED = "permission.deleted" + PERMISSION_UPDATED = "permission.updated" + SESSION_CREATED = "session.created" + SESSION_REVOKED = "session.revoked" + + @classmethod + def _missing_(cls, value: object) -> Optional["CreateWebhookEndpointDtoEvents"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +CreateWebhookEndpointDtoEventsLiteral: TypeAlias = Literal[ + "authentication.email_verification_succeeded", + "authentication.magic_auth_failed", + "authentication.magic_auth_succeeded", + "authentication.mfa_succeeded", + "authentication.oauth_failed", + "authentication.oauth_succeeded", + "authentication.password_failed", + "authentication.password_succeeded", + "authentication.passkey_failed", + "authentication.passkey_succeeded", + "authentication.sso_failed", + "authentication.sso_started", + "authentication.sso_succeeded", + "authentication.sso_timed_out", + "authentication.radar_risk_detected", + "api_key.created", + "api_key.revoked", + "connection.activated", + "connection.deactivated", + "connection.saml_certificate_renewal_required", + "connection.saml_certificate_renewed", + "connection.deleted", + "dsync.activated", + "dsync.deleted", + "dsync.group.created", + "dsync.group.deleted", + "dsync.group.updated", + "dsync.group.user_added", + "dsync.group.user_removed", + "dsync.user.created", + "dsync.user.deleted", + "dsync.user.updated", + "email_verification.created", + "flag.created", + "flag.deleted", + "flag.updated", + "flag.rule_updated", + "invitation.accepted", + "invitation.created", + "invitation.resent", + "invitation.revoked", + "magic_auth.created", + "organization.created", + "organization.deleted", + "organization.updated", + "organization_domain.created", + "organization_domain.deleted", + "organization_domain.updated", + "organization_domain.verified", + "organization_domain.verification_failed", + "password_reset.created", + "password_reset.succeeded", + "user.created", + "user.updated", + "user.deleted", + "organization_membership.created", + "organization_membership.deleted", + "organization_membership.updated", + "role.created", + "role.deleted", + "role.updated", + "organization_role.created", + "organization_role.deleted", + "organization_role.updated", + "permission.created", + "permission.deleted", + "permission.updated", + "session.created", + "session.revoked", +] diff --git a/src/workos/common/models/data_integrations_list_response_data_connected_account_state.py b/src/workos/common/models/data_integrations_list_response_data_connected_account_state.py new file mode 100644 index 00000000..cc1fa86b --- /dev/null +++ b/src/workos/common/models/data_integrations_list_response_data_connected_account_state.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .connected_account_state import ConnectedAccountState + +DataIntegrationsListResponseDataConnectedAccountState = ConnectedAccountState +__all__ = ["DataIntegrationsListResponseDataConnectedAccountState"] diff --git a/src/workos/common/models/data_integrations_list_response_data_ownership.py b/src/workos/common/models/data_integrations_list_response_data_ownership.py new file mode 100644 index 00000000..ff865b46 --- /dev/null +++ b/src/workos/common/models/data_integrations_list_response_data_ownership.py @@ -0,0 +1,32 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of data integrations list response data ownership values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class DataIntegrationsListResponseDataOwnership(str, Enum): + """Known values for DataIntegrationsListResponseDataOwnership.""" + + USERLAND_USER = "userland_user" + ORGANIZATION = "organization" + + @classmethod + def _missing_( + cls, value: object + ) -> Optional["DataIntegrationsListResponseDataOwnership"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +DataIntegrationsListResponseDataOwnershipLiteral: TypeAlias = Literal[ + "userland_user", "organization" +] diff --git a/src/workos/common/models/directory_state.py b/src/workos/common/models/directory_state.py new file mode 100644 index 00000000..0b4941d0 --- /dev/null +++ b/src/workos/common/models/directory_state.py @@ -0,0 +1,39 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of directory state values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class DirectoryState(str, Enum): + """Known values for DirectoryState.""" + + REQUIRES_TYPE = "requires_type" + LINKED = "linked" + VALIDATING = "validating" + INVALID_CREDENTIALS = "invalid_credentials" + UNLINKED = "unlinked" + DELETING = "deleting" + + @classmethod + def _missing_(cls, value: object) -> Optional["DirectoryState"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +DirectoryStateLiteral: TypeAlias = Literal[ + "requires_type", + "linked", + "validating", + "invalid_credentials", + "unlinked", + "deleting", +] diff --git a/src/workos/common/models/directory_type.py b/src/workos/common/models/directory_type.py new file mode 100644 index 00000000..f1cfb926 --- /dev/null +++ b/src/workos/common/models/directory_type.py @@ -0,0 +1,75 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of directory type values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class DirectoryType(str, Enum): + """Known values for DirectoryType.""" + + PENDING = "pending" + AZURE_SCIM_V_2_0 = "azure scim v2.0" + BAMBOOHR = "bamboohr" + BREATHE_HR = "breathe hr" + CEZANNE_HR = "cezanne hr" + CYBERARK_SCIM_V_2_0 = "cyberark scim v2.0" + FOURTH_HR = "fourth hr" + GENERIC_SCIM_V_2_0 = "generic scim v2.0" + GSUITE_DIRECTORY = "gsuite directory" + HIBOB = "hibob" + SAILPOINT_SCIM_V_2_0 = "sailpoint scim v2.0" + JUMP_CLOUD_SCIM_V_2_0 = "jump cloud scim v2.0" + OKTA_SCIM_V_2_0 = "okta scim v2.0" + ONELOGIN_SCIM_V_2_0 = "onelogin scim v2.0" + PEOPLE_HR = "people hr" + PERSONIO = "personio" + PINGFEDERATE_SCIM_V_2_0 = "pingfederate scim v2.0" + RIPPLING_SCIM_V_2_0 = "rippling scim v2.0" + S_3 = "s3" + SFTP = "sftp" + SFTP_WORKDAY = "sftp workday" + WORKDAY = "workday" + GUSTO = "gusto" + RIPPLING = "rippling" + + @classmethod + def _missing_(cls, value: object) -> Optional["DirectoryType"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +DirectoryTypeLiteral: TypeAlias = Literal[ + "pending", + "azure scim v2.0", + "bamboohr", + "breathe hr", + "cezanne hr", + "cyberark scim v2.0", + "fourth hr", + "generic scim v2.0", + "gsuite directory", + "hibob", + "sailpoint scim v2.0", + "jump cloud scim v2.0", + "okta scim v2.0", + "onelogin scim v2.0", + "people hr", + "personio", + "pingfederate scim v2.0", + "rippling scim v2.0", + "s3", + "sftp", + "sftp workday", + "workday", + "gusto", + "rippling", +] diff --git a/src/workos/common/models/directory_user_with_groups_state.py b/src/workos/common/models/directory_user_with_groups_state.py new file mode 100644 index 00000000..a70507db --- /dev/null +++ b/src/workos/common/models/directory_user_with_groups_state.py @@ -0,0 +1,31 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of directory user with groups state values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class DirectoryUserWithGroupsState(str, Enum): + """Known values for DirectoryUserWithGroupsState.""" + + ACTIVE = "active" + SUSPENDED = "suspended" + INACTIVE = "inactive" + + @classmethod + def _missing_(cls, value: object) -> Optional["DirectoryUserWithGroupsState"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +DirectoryUserWithGroupsStateLiteral: TypeAlias = Literal[ + "active", "suspended", "inactive" +] diff --git a/src/workos/common/models/generate_link_dto_intent.py b/src/workos/common/models/generate_link_dto_intent.py new file mode 100644 index 00000000..6b6be263 --- /dev/null +++ b/src/workos/common/models/generate_link_dto_intent.py @@ -0,0 +1,41 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of generate link dto intent values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class GenerateLinkDtoIntent(str, Enum): + """Known values for GenerateLinkDtoIntent.""" + + SSO = "sso" + DSYNC = "dsync" + AUDIT_LOGS = "audit_logs" + LOG_STREAMS = "log_streams" + DOMAIN_VERIFICATION = "domain_verification" + CERTIFICATE_RENEWAL = "certificate_renewal" + BRING_YOUR_OWN_KEY = "bring_your_own_key" + + @classmethod + def _missing_(cls, value: object) -> Optional["GenerateLinkDtoIntent"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +GenerateLinkDtoIntentLiteral: TypeAlias = Literal[ + "sso", + "dsync", + "audit_logs", + "log_streams", + "domain_verification", + "certificate_renewal", + "bring_your_own_key", +] diff --git a/src/workos/common/models/invitation_state.py b/src/workos/common/models/invitation_state.py new file mode 100644 index 00000000..b0ca4b0b --- /dev/null +++ b/src/workos/common/models/invitation_state.py @@ -0,0 +1,30 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of invitation state values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class InvitationState(str, Enum): + """Known values for InvitationState.""" + + PENDING = "pending" + ACCEPTED = "accepted" + EXPIRED = "expired" + REVOKED = "revoked" + + @classmethod + def _missing_(cls, value: object) -> Optional["InvitationState"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +InvitationStateLiteral: TypeAlias = Literal["pending", "accepted", "expired", "revoked"] diff --git a/src/workos/common/models/list_data_type.py b/src/workos/common/models/list_data_type.py new file mode 100644 index 00000000..235077ce --- /dev/null +++ b/src/workos/common/models/list_data_type.py @@ -0,0 +1,28 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of list data type values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class ListDataType(str, Enum): + """Known values for ListDataType.""" + + ENVIRONMENT_ROLE = "EnvironmentRole" + ORGANIZATION_ROLE = "OrganizationRole" + + @classmethod + def _missing_(cls, value: object) -> Optional["ListDataType"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +ListDataTypeLiteral: TypeAlias = Literal["EnvironmentRole", "OrganizationRole"] diff --git a/src/workos/common/models/organization_domain_data_dto_state.py b/src/workos/common/models/organization_domain_data_dto_state.py new file mode 100644 index 00000000..4d74fb7c --- /dev/null +++ b/src/workos/common/models/organization_domain_data_dto_state.py @@ -0,0 +1,28 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of organization domain data dto state values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class OrganizationDomainDataDtoState(str, Enum): + """Known values for OrganizationDomainDataDtoState.""" + + PENDING = "pending" + VERIFIED = "verified" + + @classmethod + def _missing_(cls, value: object) -> Optional["OrganizationDomainDataDtoState"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +OrganizationDomainDataDtoStateLiteral: TypeAlias = Literal["pending", "verified"] diff --git a/src/workos/common/models/organization_domain_stand_alone_state.py b/src/workos/common/models/organization_domain_stand_alone_state.py new file mode 100644 index 00000000..3ae24a5a --- /dev/null +++ b/src/workos/common/models/organization_domain_stand_alone_state.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of organization domain stand alone state values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class OrganizationDomainStandAloneState(str, Enum): + """Known values for OrganizationDomainStandAloneState.""" + + FAILED = "failed" + LEGACY_VERIFIED = "legacy_verified" + PENDING = "pending" + UNVERIFIED = "unverified" + VERIFIED = "verified" + + @classmethod + def _missing_(cls, value: object) -> Optional["OrganizationDomainStandAloneState"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +OrganizationDomainStandAloneStateLiteral: TypeAlias = Literal[ + "failed", "legacy_verified", "pending", "unverified", "verified" +] diff --git a/src/workos/common/models/organization_domain_stand_alone_verification_strategy.py b/src/workos/common/models/organization_domain_stand_alone_verification_strategy.py new file mode 100644 index 00000000..a313a7e4 --- /dev/null +++ b/src/workos/common/models/organization_domain_stand_alone_verification_strategy.py @@ -0,0 +1,32 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of organization domain stand alone verification strategy values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class OrganizationDomainStandAloneVerificationStrategy(str, Enum): + """Known values for OrganizationDomainStandAloneVerificationStrategy.""" + + DNS = "dns" + MANUAL = "manual" + + @classmethod + def _missing_( + cls, value: object + ) -> Optional["OrganizationDomainStandAloneVerificationStrategy"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +OrganizationDomainStandAloneVerificationStrategyLiteral: TypeAlias = Literal[ + "dns", "manual" +] diff --git a/src/workos/common/models/organization_domain_state.py b/src/workos/common/models/organization_domain_state.py new file mode 100644 index 00000000..d1134d1c --- /dev/null +++ b/src/workos/common/models/organization_domain_state.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .organization_domain_stand_alone_state import OrganizationDomainStandAloneState + +OrganizationDomainState = OrganizationDomainStandAloneState +__all__ = ["OrganizationDomainState"] diff --git a/src/workos/common/models/organization_domain_verification_strategy.py b/src/workos/common/models/organization_domain_verification_strategy.py new file mode 100644 index 00000000..83c76677 --- /dev/null +++ b/src/workos/common/models/organization_domain_verification_strategy.py @@ -0,0 +1,10 @@ +# This file is auto-generated by oagen. Do not edit. + +from .organization_domain_stand_alone_verification_strategy import ( + OrganizationDomainStandAloneVerificationStrategy, +) + +OrganizationDomainVerificationStrategy = ( + OrganizationDomainStandAloneVerificationStrategy +) +__all__ = ["OrganizationDomainVerificationStrategy"] diff --git a/src/workos/common/models/organization_membership_status.py b/src/workos/common/models/organization_membership_status.py new file mode 100644 index 00000000..fac604a1 --- /dev/null +++ b/src/workos/common/models/organization_membership_status.py @@ -0,0 +1,31 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of organization membership status values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class OrganizationMembershipStatus(str, Enum): + """Known values for OrganizationMembershipStatus.""" + + ACTIVE = "active" + INACTIVE = "inactive" + PENDING = "pending" + + @classmethod + def _missing_(cls, value: object) -> Optional["OrganizationMembershipStatus"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +OrganizationMembershipStatusLiteral: TypeAlias = Literal[ + "active", "inactive", "pending" +] diff --git a/src/workos/common/models/password_hash_type.py b/src/workos/common/models/password_hash_type.py new file mode 100644 index 00000000..425b5a6c --- /dev/null +++ b/src/workos/common/models/password_hash_type.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .create_user_dto_password_hash_type import CreateUserDtoPasswordHashType + +PasswordHashType = CreateUserDtoPasswordHashType +__all__ = ["PasswordHashType"] diff --git a/src/workos/common/models/profile_connection_type.py b/src/workos/common/models/profile_connection_type.py new file mode 100644 index 00000000..0b92b4fe --- /dev/null +++ b/src/workos/common/models/profile_connection_type.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .connection_type import ConnectionType + +ProfileConnectionType = ConnectionType +__all__ = ["ProfileConnectionType"] diff --git a/src/workos/common/models/radar_standalone_assess_request_action.py b/src/workos/common/models/radar_standalone_assess_request_action.py new file mode 100644 index 00000000..97fc8e4d --- /dev/null +++ b/src/workos/common/models/radar_standalone_assess_request_action.py @@ -0,0 +1,36 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of radar standalone assess request action values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class RadarStandaloneAssessRequestAction(str, Enum): + """Known values for RadarStandaloneAssessRequestAction.""" + + LOGIN = "login" + SIGNUP = "signup" + SIGN_UP = "sign-up" + SIGN_IN = "sign-in" + SIGN_UP_2 = "sign_up" + SIGN_IN_2 = "sign_in" + SIGN_IN_3 = "sign in" + SIGN_UP_3 = "sign up" + + @classmethod + def _missing_(cls, value: object) -> Optional["RadarStandaloneAssessRequestAction"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +RadarStandaloneAssessRequestActionLiteral: TypeAlias = Literal[ + "login", "signup", "sign-up", "sign-in", "sign_up", "sign_in", "sign in", "sign up" +] diff --git a/src/workos/common/models/radar_standalone_assess_request_auth_method.py b/src/workos/common/models/radar_standalone_assess_request_auth_method.py new file mode 100644 index 00000000..934c0088 --- /dev/null +++ b/src/workos/common/models/radar_standalone_assess_request_auth_method.py @@ -0,0 +1,45 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of radar standalone assess request auth method values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class RadarStandaloneAssessRequestAuthMethod(str, Enum): + """Known values for RadarStandaloneAssessRequestAuthMethod.""" + + PASSWORD = "Password" + PASSKEY = "Passkey" + AUTHENTICATOR = "Authenticator" + SMS_OTP = "SMS_OTP" + EMAIL_OTP = "Email_OTP" + SOCIAL = "Social" + SSO = "SSO" + OTHER = "Other" + + @classmethod + def _missing_( + cls, value: object + ) -> Optional["RadarStandaloneAssessRequestAuthMethod"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +RadarStandaloneAssessRequestAuthMethodLiteral: TypeAlias = Literal[ + "Password", + "Passkey", + "Authenticator", + "SMS_OTP", + "Email_OTP", + "Social", + "SSO", + "Other", +] diff --git a/src/workos/common/models/radar_standalone_response_blocklist_type.py b/src/workos/common/models/radar_standalone_response_blocklist_type.py new file mode 100644 index 00000000..a26a2a5a --- /dev/null +++ b/src/workos/common/models/radar_standalone_response_blocklist_type.py @@ -0,0 +1,43 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of radar standalone response blocklist type values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class RadarStandaloneResponseBlocklistType(str, Enum): + """Known values for RadarStandaloneResponseBlocklistType.""" + + IP_ADDRESS = "ip_address" + DOMAIN = "domain" + EMAIL = "email" + DEVICE = "device" + USER_AGENT = "user_agent" + DEVICE_FINGERPRINT = "device_fingerprint" + COUNTRY = "country" + + @classmethod + def _missing_( + cls, value: object + ) -> Optional["RadarStandaloneResponseBlocklistType"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +RadarStandaloneResponseBlocklistTypeLiteral: TypeAlias = Literal[ + "ip_address", + "domain", + "email", + "device", + "user_agent", + "device_fingerprint", + "country", +] diff --git a/src/workos/common/models/radar_standalone_response_control.py b/src/workos/common/models/radar_standalone_response_control.py new file mode 100644 index 00000000..006bf54f --- /dev/null +++ b/src/workos/common/models/radar_standalone_response_control.py @@ -0,0 +1,47 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of radar standalone response control values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class RadarStandaloneResponseControl(str, Enum): + """Known values for RadarStandaloneResponseControl.""" + + BOT_DETECTION = "bot_detection" + BRUTE_FORCE_ATTACK = "brute_force_attack" + CREDENTIAL_STUFFING = "credential_stuffing" + DOMAIN_SIGN_UP_RATE_LIMIT = "domain_sign_up_rate_limit" + IP_SIGN_UP_RATE_LIMIT = "ip_sign_up_rate_limit" + IMPOSSIBLE_TRAVEL = "impossible_travel" + REPEAT_SIGN_UP = "repeat_sign_up" + STALE_ACCOUNT = "stale_account" + UNRECOGNIZED_DEVICE = "unrecognized_device" + RESTRICTION = "restriction" + + @classmethod + def _missing_(cls, value: object) -> Optional["RadarStandaloneResponseControl"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +RadarStandaloneResponseControlLiteral: TypeAlias = Literal[ + "bot_detection", + "brute_force_attack", + "credential_stuffing", + "domain_sign_up_rate_limit", + "ip_sign_up_rate_limit", + "impossible_travel", + "repeat_sign_up", + "stale_account", + "unrecognized_device", + "restriction", +] diff --git a/src/workos/common/models/radar_standalone_response_verdict.py b/src/workos/common/models/radar_standalone_response_verdict.py new file mode 100644 index 00000000..647672b0 --- /dev/null +++ b/src/workos/common/models/radar_standalone_response_verdict.py @@ -0,0 +1,31 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of radar standalone response verdict values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class RadarStandaloneResponseVerdict(str, Enum): + """Known values for RadarStandaloneResponseVerdict.""" + + ALLOW = "allow" + BLOCK = "block" + CHALLENGE = "challenge" + + @classmethod + def _missing_(cls, value: object) -> Optional["RadarStandaloneResponseVerdict"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +RadarStandaloneResponseVerdictLiteral: TypeAlias = Literal[ + "allow", "block", "challenge" +] diff --git a/src/workos/common/models/resend_user_invite_options_dto_locale.py b/src/workos/common/models/resend_user_invite_options_dto_locale.py new file mode 100644 index 00000000..c635f359 --- /dev/null +++ b/src/workos/common/models/resend_user_invite_options_dto_locale.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .create_user_invite_options_dto_locale import CreateUserInviteOptionsDtoLocale + +ResendUserInviteOptionsDtoLocale = CreateUserInviteOptionsDtoLocale +__all__ = ["ResendUserInviteOptionsDtoLocale"] diff --git a/src/workos/common/models/role_type.py b/src/workos/common/models/role_type.py new file mode 100644 index 00000000..c8bce986 --- /dev/null +++ b/src/workos/common/models/role_type.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .list_data_type import ListDataType + +RoleType = ListDataType +__all__ = ["RoleType"] diff --git a/src/workos/common/models/update_user_dto_password_hash_type.py b/src/workos/common/models/update_user_dto_password_hash_type.py new file mode 100644 index 00000000..1d242926 --- /dev/null +++ b/src/workos/common/models/update_user_dto_password_hash_type.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .create_user_dto_password_hash_type import CreateUserDtoPasswordHashType + +UpdateUserDtoPasswordHashType = CreateUserDtoPasswordHashType +__all__ = ["UpdateUserDtoPasswordHashType"] diff --git a/src/workos/common/models/update_webhook_endpoint_dto_events.py b/src/workos/common/models/update_webhook_endpoint_dto_events.py new file mode 100644 index 00000000..349a70ac --- /dev/null +++ b/src/workos/common/models/update_webhook_endpoint_dto_events.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .create_webhook_endpoint_dto_events import CreateWebhookEndpointDtoEvents + +UpdateWebhookEndpointDtoEvents = CreateWebhookEndpointDtoEvents +__all__ = ["UpdateWebhookEndpointDtoEvents"] diff --git a/src/workos/common/models/update_webhook_endpoint_dto_status.py b/src/workos/common/models/update_webhook_endpoint_dto_status.py new file mode 100644 index 00000000..93f715be --- /dev/null +++ b/src/workos/common/models/update_webhook_endpoint_dto_status.py @@ -0,0 +1,28 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of update webhook endpoint dto status values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class UpdateWebhookEndpointDtoStatus(str, Enum): + """Known values for UpdateWebhookEndpointDtoStatus.""" + + ENABLED = "enabled" + DISABLED = "disabled" + + @classmethod + def _missing_(cls, value: object) -> Optional["UpdateWebhookEndpointDtoStatus"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +UpdateWebhookEndpointDtoStatusLiteral: TypeAlias = Literal["enabled", "disabled"] diff --git a/src/workos/common/models/user_identities_get_item_provider.py b/src/workos/common/models/user_identities_get_item_provider.py new file mode 100644 index 00000000..2dce1e73 --- /dev/null +++ b/src/workos/common/models/user_identities_get_item_provider.py @@ -0,0 +1,55 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of user identities get item provider values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class UserIdentitiesGetItemProvider(str, Enum): + """Known values for UserIdentitiesGetItemProvider.""" + + APPLE_OAUTH = "AppleOAuth" + BITBUCKET_OAUTH = "BitbucketOAuth" + DISCORD_OAUTH = "DiscordOAuth" + GITHUB_OAUTH = "GithubOAuth" + GIT_LAB_OAUTH = "GitLabOAuth" + GOOGLE_OAUTH = "GoogleOAuth" + INTUIT_OAUTH = "IntuitOAuth" + LINKED_IN_OAUTH = "LinkedInOAuth" + MICROSOFT_OAUTH = "MicrosoftOAuth" + SALESFORCE_OAUTH = "SalesforceOAuth" + SLACK_OAUTH = "SlackOAuth" + VERCEL_MARKETPLACE_OAUTH = "VercelMarketplaceOAuth" + VERCEL_OAUTH = "VercelOAuth" + XERO_OAUTH = "XeroOAuth" + + @classmethod + def _missing_(cls, value: object) -> Optional["UserIdentitiesGetItemProvider"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +UserIdentitiesGetItemProviderLiteral: TypeAlias = Literal[ + "AppleOAuth", + "BitbucketOAuth", + "DiscordOAuth", + "GithubOAuth", + "GitLabOAuth", + "GoogleOAuth", + "IntuitOAuth", + "LinkedInOAuth", + "MicrosoftOAuth", + "SalesforceOAuth", + "SlackOAuth", + "VercelMarketplaceOAuth", + "VercelOAuth", + "XeroOAuth", +] diff --git a/src/workos/common/models/user_invite_state.py b/src/workos/common/models/user_invite_state.py new file mode 100644 index 00000000..556485ac --- /dev/null +++ b/src/workos/common/models/user_invite_state.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .invitation_state import InvitationState + +UserInviteState = InvitationState +__all__ = ["UserInviteState"] diff --git a/src/workos/common/models/user_organization_membership_base_list_data_status.py b/src/workos/common/models/user_organization_membership_base_list_data_status.py new file mode 100644 index 00000000..784a967b --- /dev/null +++ b/src/workos/common/models/user_organization_membership_base_list_data_status.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .organization_membership_status import OrganizationMembershipStatus + +UserOrganizationMembershipBaseListDataStatus = OrganizationMembershipStatus +__all__ = ["UserOrganizationMembershipBaseListDataStatus"] diff --git a/src/workos/common/models/user_organization_membership_status.py b/src/workos/common/models/user_organization_membership_status.py new file mode 100644 index 00000000..de286bb4 --- /dev/null +++ b/src/workos/common/models/user_organization_membership_status.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .organization_membership_status import OrganizationMembershipStatus + +UserOrganizationMembershipStatus = OrganizationMembershipStatus +__all__ = ["UserOrganizationMembershipStatus"] diff --git a/src/workos/common/models/user_sessions_auth_method.py b/src/workos/common/models/user_sessions_auth_method.py new file mode 100644 index 00000000..64018e17 --- /dev/null +++ b/src/workos/common/models/user_sessions_auth_method.py @@ -0,0 +1,47 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of user sessions auth method values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class UserSessionsAuthMethod(str, Enum): + """Known values for UserSessionsAuthMethod.""" + + CROSS_APP_AUTH = "cross_app_auth" + EXTERNAL_AUTH = "external_auth" + IMPERSONATION = "impersonation" + MAGIC_CODE = "magic_code" + MIGRATED_SESSION = "migrated_session" + OAUTH = "oauth" + PASSKEY = "passkey" + PASSWORD = "password" + SSO = "sso" + UNKNOWN = "unknown" + + @classmethod + def _missing_(cls, value: object) -> Optional["UserSessionsAuthMethod"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +UserSessionsAuthMethodLiteral: TypeAlias = Literal[ + "cross_app_auth", + "external_auth", + "impersonation", + "magic_code", + "migrated_session", + "oauth", + "passkey", + "password", + "sso", + "unknown", +] diff --git a/src/workos/common/models/user_sessions_status.py b/src/workos/common/models/user_sessions_status.py new file mode 100644 index 00000000..7a19bd2c --- /dev/null +++ b/src/workos/common/models/user_sessions_status.py @@ -0,0 +1,29 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of user sessions status values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class UserSessionsStatus(str, Enum): + """Known values for UserSessionsStatus.""" + + ACTIVE = "active" + EXPIRED = "expired" + REVOKED = "revoked" + + @classmethod + def _missing_(cls, value: object) -> Optional["UserSessionsStatus"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +UserSessionsStatusLiteral: TypeAlias = Literal["active", "expired", "revoked"] diff --git a/src/workos/common/models/webhook_endpoint_json_status.py b/src/workos/common/models/webhook_endpoint_json_status.py new file mode 100644 index 00000000..909353ad --- /dev/null +++ b/src/workos/common/models/webhook_endpoint_json_status.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .update_webhook_endpoint_dto_status import UpdateWebhookEndpointDtoStatus + +WebhookEndpointJsonStatus = UpdateWebhookEndpointDtoStatus +__all__ = ["WebhookEndpointJsonStatus"] diff --git a/src/workos/common/models/widget_session_token_dto_scopes.py b/src/workos/common/models/widget_session_token_dto_scopes.py new file mode 100644 index 00000000..5e72dbf0 --- /dev/null +++ b/src/workos/common/models/widget_session_token_dto_scopes.py @@ -0,0 +1,39 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of widget session token dto scopes values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class WidgetSessionTokenDtoScopes(str, Enum): + """Known values for WidgetSessionTokenDtoScopes.""" + + WIDGETS_USERS_TABLE_MANAGE = "widgets:users-table:manage" + WIDGETS_DOMAIN_VERIFICATION_MANAGE = "widgets:domain-verification:manage" + WIDGETS_SSO_MANAGE = "widgets:sso:manage" + WIDGETS_API_KEYS_MANAGE = "widgets:api-keys:manage" + WIDGETS_DSYNC_MANAGE = "widgets:dsync:manage" + WIDGETS_AUDIT_LOG_STREAMING_MANAGE = "widgets:audit-log-streaming:manage" + + @classmethod + def _missing_(cls, value: object) -> Optional["WidgetSessionTokenDtoScopes"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +WidgetSessionTokenDtoScopesLiteral: TypeAlias = Literal[ + "widgets:users-table:manage", + "widgets:domain-verification:manage", + "widgets:sso:manage", + "widgets:api-keys:manage", + "widgets:dsync:manage", + "widgets:audit-log-streaming:manage", +] diff --git a/src/workos/connections/__init__.py b/src/workos/connections/__init__.py new file mode 100644 index 00000000..06266f9e --- /dev/null +++ b/src/workos/connections/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import Connections, AsyncConnections +from .models import * diff --git a/src/workos/connections/_resource.py b/src/workos/connections/_resource.py new file mode 100644 index 00000000..5fec78a4 --- /dev/null +++ b/src/workos/connections/_resource.py @@ -0,0 +1,267 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import Connection +from .models import ConnectionsConnectionType, ConnectionsOrder +from .._pagination import AsyncPage, SyncPage +from .._types import RequestOptions + + +class Connections: + """Connections API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[ConnectionsOrder] = None, + connection_type: Optional[ConnectionsConnectionType] = None, + domain: Optional[str] = None, + organization_id: Optional[str] = None, + search: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[Connection]: + """List Connections + + Get a list of all of your existing connections matching the criteria specified. + + Args: + connection_type: Filter Connections by their type. + domain: Filter Connections by their associated domain. + organization_id: Filter Connections by their associated organization. + search: Searchable text to match against Connection names. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[Connection] + + Raises: + AuthorizationException: If the request is forbidden (403). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "connection_type": connection_type.value if connection_type else None, + "domain": domain, + "organization_id": organization_id, + "search": search, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path="connections", + model=Connection, + params=params, + request_options=request_options, + ) + + def get( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Connection: + """Get a Connection + + Get the details of an existing connection. + + Args: + id: Unique identifier for the Connection. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Connection + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"connections/{id}", + model=Connection, + request_options=request_options, + ) + + find = get + + def delete( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a Connection + + Permanently deletes an existing connection. It cannot be undone. + + Args: + id: Unique identifier for the Connection. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + self._client.request( + method="delete", + path=f"connections/{id}", + request_options=request_options, + ) + + +class AsyncConnections: + """Connections API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[ConnectionsOrder] = None, + connection_type: Optional[ConnectionsConnectionType] = None, + domain: Optional[str] = None, + organization_id: Optional[str] = None, + search: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[Connection]: + """List Connections + + Get a list of all of your existing connections matching the criteria specified. + + Args: + connection_type: Filter Connections by their type. + domain: Filter Connections by their associated domain. + organization_id: Filter Connections by their associated organization. + search: Searchable text to match against Connection names. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[Connection] + + Raises: + AuthorizationException: If the request is forbidden (403). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "connection_type": connection_type.value if connection_type else None, + "domain": domain, + "organization_id": organization_id, + "search": search, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path="connections", + model=Connection, + params=params, + request_options=request_options, + ) + + async def get( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Connection: + """Get a Connection + + Get the details of an existing connection. + + Args: + id: Unique identifier for the Connection. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Connection + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"connections/{id}", + model=Connection, + request_options=request_options, + ) + + find = get + + async def delete( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a Connection + + Permanently deletes an existing connection. It cannot be undone. + + Args: + id: Unique identifier for the Connection. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + await self._client.request( + method="delete", + path=f"connections/{id}", + request_options=request_options, + ) diff --git a/src/workos/connections/models/__init__.py b/src/workos/connections/models/__init__.py new file mode 100644 index 00000000..e19062df --- /dev/null +++ b/src/workos/connections/models/__init__.py @@ -0,0 +1,9 @@ +# This file is auto-generated by oagen. Do not edit. + +from .connection import Connection as Connection +from .connection_domain import ConnectionDomain as ConnectionDomain +from .connection_option import ConnectionOption as ConnectionOption +from .connections_connection_type import ( + ConnectionsConnectionType as ConnectionsConnectionType, +) +from .connections_order import ConnectionsOrder as ConnectionsOrder diff --git a/src/workos/connections/models/connection.py b/src/workos/connections/models/connection.py new file mode 100644 index 00000000..36d5f8fd --- /dev/null +++ b/src/workos/connections/models/connection.py @@ -0,0 +1,99 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import cast +from typing import Any, Dict, List, Literal, Optional +from workos._errors import BaseRequestException + +from .connection_domain import ConnectionDomain +from .connection_option import ConnectionOption +from workos.common.models import ConnectionState +from workos.common.models import ConnectionStatus +from workos.common.models import ConnectionType + + +@dataclass(slots=True) +class Connection: + """Connection model.""" + + object: Literal["connection"] + """Distinguishes the Connection object.""" + id: str + """Unique identifier for the Connection.""" + connection_type: "ConnectionType" + """The type of the SSO Connection used to authenticate the user. The Connection type may be used to dynamically generate authorization URLs.""" + name: str + """A human-readable name for the Connection. This will most commonly be the organization's name.""" + state: "ConnectionState" + """Indicates whether a Connection is able to authenticate users.""" + domains: List["ConnectionDomain"] + """List of Organization Domains.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + organization_id: Optional[str] = None + """Unique identifier for the Organization in which the Connection resides.""" + status: Optional["ConnectionStatus"] = None + """Deprecated. Use `state` instead.""" + options: Optional["ConnectionOption"] = None + """Configuration options for SAML connections. Only present for SAML connection types.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Connection": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + connection_type=ConnectionType(data["connection_type"]), + name=data["name"], + state=ConnectionState(data["state"]), + domains=[ + ConnectionDomain.from_dict(cast(Dict[str, Any], item)) + for item in cast(list[Any], data["domains"]) + ], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + organization_id=data.get("organization_id"), + status=ConnectionStatus(_v) + if (_v := data.get("status")) is not None + else None, + options=ConnectionOption.from_dict(cast(Dict[str, Any], _v)) + if (_v := data.get("options")) is not None + else None, + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing Connection: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["connection_type"] = self.connection_type + result["name"] = self.name + result["state"] = self.state + result["domains"] = [item.to_dict() for item in self.domains] + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + if self.organization_id is not None: + result["organization_id"] = self.organization_id + if self.status is not None: + result["status"] = self.status + if self.options is not None: + result["options"] = self.options.to_dict() + return result diff --git a/src/workos/connections/models/connection_domain.py b/src/workos/connections/models/connection_domain.py new file mode 100644 index 00000000..d10e9841 --- /dev/null +++ b/src/workos/connections/models/connection_domain.py @@ -0,0 +1,41 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Literal +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class ConnectionDomain: + """Connection Domain model.""" + + id: str + """Unique identifier for the Connection Domain.""" + object: Literal["connection_domain"] + """Distinguishes the Connection Domain object.""" + domain: str + """The domain value.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ConnectionDomain": + """Deserialize from a dictionary.""" + try: + return cls( + id=data["id"], + object=data["object"], + domain=data["domain"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing ConnectionDomain: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["id"] = self.id + result["object"] = self.object + result["domain"] = self.domain + return result diff --git a/src/workos/connections/models/connection_option.py b/src/workos/connections/models/connection_option.py new file mode 100644 index 00000000..fa387e76 --- /dev/null +++ b/src/workos/connections/models/connection_option.py @@ -0,0 +1,36 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class ConnectionOption: + """Configuration options for SAML connections. Only present for SAML connection types.""" + + signing_cert: Optional[str] + """The signing certificate of the SAML connection.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ConnectionOption": + """Deserialize from a dictionary.""" + try: + return cls( + signing_cert=data["signing_cert"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing ConnectionOption: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + if self.signing_cert is not None: + result["signing_cert"] = self.signing_cert + else: + result["signing_cert"] = None + return result diff --git a/src/workos/connections/models/connections_connection_type.py b/src/workos/connections/models/connections_connection_type.py new file mode 100644 index 00000000..c99aa259 --- /dev/null +++ b/src/workos/connections/models/connections_connection_type.py @@ -0,0 +1,123 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of connections connection type values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class ConnectionsConnectionType(str, Enum): + """Known values for ConnectionsConnectionType.""" + + ADFSSAML = "ADFSSAML" + ADP_OIDC = "AdpOidc" + APPLE_OAUTH = "AppleOAuth" + AUTH_0_SAML = "Auth0SAML" + AZURE_SAML = "AzureSAML" + BITBUCKET_OAUTH = "BitbucketOAuth" + CAS_SAML = "CasSAML" + CLOUDFLARE_SAML = "CloudflareSAML" + CLASS_LINK_SAML = "ClassLinkSAML" + CLEVER_OIDC = "CleverOIDC" + CYBER_ARK_SAML = "CyberArkSAML" + DISCORD_OAUTH = "DiscordOAuth" + DUO_SAML = "DuoSAML" + ENTRA_ID_OIDC = "EntraIdOIDC" + GENERIC_OIDC = "GenericOIDC" + GENERIC_SAML = "GenericSAML" + GITHUB_OAUTH = "GithubOAuth" + GIT_LAB_OAUTH = "GitLabOAuth" + GOOGLE_OAUTH = "GoogleOAuth" + GOOGLE_OIDC = "GoogleOIDC" + GOOGLE_SAML = "GoogleSAML" + INTUIT_OAUTH = "IntuitOAuth" + JUMP_CLOUD_SAML = "JumpCloudSAML" + KEYCLOAK_SAML = "KeycloakSAML" + LAST_PASS_SAML = "LastPassSAML" + LINKED_IN_OAUTH = "LinkedInOAuth" + LOGIN_GOV_OIDC = "LoginGovOidc" + MAGIC_LINK = "MagicLink" + MICROSOFT_OAUTH = "MicrosoftOAuth" + MINI_ORANGE_SAML = "MiniOrangeSAML" + NET_IQ_SAML = "NetIqSAML" + OKTA_OIDC = "OktaOIDC" + OKTA_SAML = "OktaSAML" + ONE_LOGIN_SAML = "OneLoginSAML" + ORACLE_SAML = "OracleSAML" + PING_FEDERATE_SAML = "PingFederateSAML" + PING_ONE_SAML = "PingOneSAML" + RIPPLING_SAML = "RipplingSAML" + SALESFORCE_SAML = "SalesforceSAML" + SHIBBOLETH_GENERIC_SAML = "ShibbolethGenericSAML" + SHIBBOLETH_SAML = "ShibbolethSAML" + SIMPLE_SAML_PHP_SAML = "SimpleSamlPhpSAML" + SALESFORCE_OAUTH = "SalesforceOAuth" + SLACK_OAUTH = "SlackOAuth" + VERCEL_MARKETPLACE_OAUTH = "VercelMarketplaceOAuth" + VERCEL_OAUTH = "VercelOAuth" + V_MWARE_SAML = "VMwareSAML" + XERO_OAUTH = "XeroOAuth" + + @classmethod + def _missing_(cls, value: object) -> Optional["ConnectionsConnectionType"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +ConnectionsConnectionTypeLiteral: TypeAlias = Literal[ + "ADFSSAML", + "AdpOidc", + "AppleOAuth", + "Auth0SAML", + "AzureSAML", + "BitbucketOAuth", + "CasSAML", + "CloudflareSAML", + "ClassLinkSAML", + "CleverOIDC", + "CyberArkSAML", + "DiscordOAuth", + "DuoSAML", + "EntraIdOIDC", + "GenericOIDC", + "GenericSAML", + "GithubOAuth", + "GitLabOAuth", + "GoogleOAuth", + "GoogleOIDC", + "GoogleSAML", + "IntuitOAuth", + "JumpCloudSAML", + "KeycloakSAML", + "LastPassSAML", + "LinkedInOAuth", + "LoginGovOidc", + "MagicLink", + "MicrosoftOAuth", + "MiniOrangeSAML", + "NetIqSAML", + "OktaOIDC", + "OktaSAML", + "OneLoginSAML", + "OracleSAML", + "PingFederateSAML", + "PingOneSAML", + "RipplingSAML", + "SalesforceSAML", + "ShibbolethGenericSAML", + "ShibbolethSAML", + "SimpleSamlPhpSAML", + "SalesforceOAuth", + "SlackOAuth", + "VercelMarketplaceOAuth", + "VercelOAuth", + "VMwareSAML", + "XeroOAuth", +] diff --git a/src/workos/connections/models/connections_order.py b/src/workos/connections/models/connections_order.py new file mode 100644 index 00000000..31b84d65 --- /dev/null +++ b/src/workos/connections/models/connections_order.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import ApplicationsOrder + +ConnectionsOrder = ApplicationsOrder +__all__ = ["ConnectionsOrder"] diff --git a/src/workos/directories/__init__.py b/src/workos/directories/__init__.py new file mode 100644 index 00000000..9c516f50 --- /dev/null +++ b/src/workos/directories/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import Directories, AsyncDirectories +from .models import * diff --git a/src/workos/directories/_resource.py b/src/workos/directories/_resource.py new file mode 100644 index 00000000..01350170 --- /dev/null +++ b/src/workos/directories/_resource.py @@ -0,0 +1,263 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import Directory +from .models import DirectoriesOrder +from .._pagination import AsyncPage, SyncPage +from .._types import RequestOptions + + +class Directories: + """Directories API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[DirectoriesOrder] = None, + organization_id: Optional[str] = None, + search: Optional[str] = None, + domain: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[Directory]: + """List Directories + + Get a list of all of your existing directories matching the criteria specified. + + Args: + organization_id: Filter Directories by their associated organization. + search: Searchable text to match against Directory names. + domain: Filter Directories by their associated domain. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[Directory] + + Raises: + AuthorizationException: If the request is forbidden (403). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "organization_id": organization_id, + "search": search, + "domain": domain, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path="directories", + model=Directory, + params=params, + request_options=request_options, + ) + + def get( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Directory: + """Get a Directory + + Get the details of an existing directory. + + Args: + id: Unique identifier for the Directory. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Directory + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"directories/{id}", + model=Directory, + request_options=request_options, + ) + + find = get + + def delete( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a Directory + + Permanently deletes an existing directory. It cannot be undone. + + Args: + id: Unique identifier for the Directory. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + AuthorizationException: If the request is forbidden (403). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + self._client.request( + method="delete", + path=f"directories/{id}", + request_options=request_options, + ) + + delete_directory = delete + + +class AsyncDirectories: + """Directories API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[DirectoriesOrder] = None, + organization_id: Optional[str] = None, + search: Optional[str] = None, + domain: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[Directory]: + """List Directories + + Get a list of all of your existing directories matching the criteria specified. + + Args: + organization_id: Filter Directories by their associated organization. + search: Searchable text to match against Directory names. + domain: Filter Directories by their associated domain. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[Directory] + + Raises: + AuthorizationException: If the request is forbidden (403). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "organization_id": organization_id, + "search": search, + "domain": domain, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path="directories", + model=Directory, + params=params, + request_options=request_options, + ) + + async def get( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Directory: + """Get a Directory + + Get the details of an existing directory. + + Args: + id: Unique identifier for the Directory. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Directory + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"directories/{id}", + model=Directory, + request_options=request_options, + ) + + find = get + + async def delete( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a Directory + + Permanently deletes an existing directory. It cannot be undone. + + Args: + id: Unique identifier for the Directory. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + AuthorizationException: If the request is forbidden (403). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + await self._client.request( + method="delete", + path=f"directories/{id}", + request_options=request_options, + ) + + delete_directory = delete diff --git a/src/workos/directories/models/__init__.py b/src/workos/directories/models/__init__.py new file mode 100644 index 00000000..e0967854 --- /dev/null +++ b/src/workos/directories/models/__init__.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .directories_order import DirectoriesOrder as DirectoriesOrder +from .directory import Directory as Directory +from .directory_metadata import DirectoryMetadata as DirectoryMetadata +from .directory_metadata_user import DirectoryMetadataUser as DirectoryMetadataUser diff --git a/src/workos/directories/models/directories_order.py b/src/workos/directories/models/directories_order.py new file mode 100644 index 00000000..6a0bbe54 --- /dev/null +++ b/src/workos/directories/models/directories_order.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import ApplicationsOrder + +DirectoriesOrder = ApplicationsOrder +__all__ = ["DirectoriesOrder"] diff --git a/src/workos/directories/models/directory.py b/src/workos/directories/models/directory.py new file mode 100644 index 00000000..fe8299f9 --- /dev/null +++ b/src/workos/directories/models/directory.py @@ -0,0 +1,91 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import cast +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + +from .directory_metadata import DirectoryMetadata +from workos.common.models import DirectoryState +from workos.common.models import DirectoryType + + +@dataclass(slots=True) +class Directory: + """Directory model.""" + + object: Literal["directory"] + """Distinguishes the Directory object.""" + id: str + """Unique identifier for the Directory.""" + organization_id: str + """The unique identifier for the Organization in which the directory resides.""" + external_key: str + """External Key for the Directory.""" + type: "DirectoryType" + """The type of external Directory Provider integrated with.""" + state: "DirectoryState" + """Describes whether the Directory has been successfully connected to an external provider.""" + name: str + """The name of the directory.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + domain: Optional[str] = None + """The URL associated with an Enterprise Client.""" + metadata: Optional["DirectoryMetadata"] = None + """Aggregate counts of directory users and groups synced from the provider.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Directory": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + organization_id=data["organization_id"], + external_key=data["external_key"], + type=DirectoryType(data["type"]), + state=DirectoryState(data["state"]), + name=data["name"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + domain=data.get("domain"), + metadata=DirectoryMetadata.from_dict(cast(Dict[str, Any], _v)) + if (_v := data.get("metadata")) is not None + else None, + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing Directory: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["organization_id"] = self.organization_id + result["external_key"] = self.external_key + result["type"] = self.type + result["state"] = self.state + result["name"] = self.name + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + if self.domain is not None: + result["domain"] = self.domain + if self.metadata is not None: + result["metadata"] = self.metadata.to_dict() + return result diff --git a/src/workos/directories/models/directory_metadata.py b/src/workos/directories/models/directory_metadata.py new file mode 100644 index 00000000..2019f340 --- /dev/null +++ b/src/workos/directories/models/directory_metadata.py @@ -0,0 +1,42 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict +from workos._errors import BaseRequestException + +from .directory_metadata_user import DirectoryMetadataUser + + +@dataclass(slots=True) +class DirectoryMetadata: + """Aggregate counts of directory users and groups synced from the provider.""" + + users: "DirectoryMetadataUser" + """Counts of active and inactive directory users.""" + groups: int + """Count of directory groups.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "DirectoryMetadata": + """Deserialize from a dictionary.""" + try: + return cls( + users=DirectoryMetadataUser.from_dict( + cast(Dict[str, Any], data["users"]) + ), + groups=data["groups"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing DirectoryMetadata: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["users"] = self.users.to_dict() + result["groups"] = self.groups + return result diff --git a/src/workos/directories/models/directory_metadata_user.py b/src/workos/directories/models/directory_metadata_user.py new file mode 100644 index 00000000..31e41a47 --- /dev/null +++ b/src/workos/directories/models/directory_metadata_user.py @@ -0,0 +1,37 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class DirectoryMetadataUser: + """Counts of active and inactive directory users.""" + + active: int + """Count of active directory users.""" + inactive: int + """Count of inactive directory users.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "DirectoryMetadataUser": + """Deserialize from a dictionary.""" + try: + return cls( + active=data["active"], + inactive=data["inactive"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing DirectoryMetadataUser: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["active"] = self.active + result["inactive"] = self.inactive + return result diff --git a/src/workos/directory_groups/__init__.py b/src/workos/directory_groups/__init__.py new file mode 100644 index 00000000..17f7a5ee --- /dev/null +++ b/src/workos/directory_groups/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import DirectoryGroups, AsyncDirectoryGroups +from .models import * diff --git a/src/workos/directory_groups/_resource.py b/src/workos/directory_groups/_resource.py new file mode 100644 index 00000000..66cc6003 --- /dev/null +++ b/src/workos/directory_groups/_resource.py @@ -0,0 +1,203 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import DirectoryGroup +from .models import DirectoryGroupsOrder +from .._pagination import AsyncPage, SyncPage +from .._types import RequestOptions + + +class DirectoryGroups: + """Directory Groups API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[DirectoryGroupsOrder] = None, + directory: Optional[str] = None, + user: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[DirectoryGroup]: + """List Directory Groups + + Get a list of all of existing directory groups matching the criteria specified. + + Args: + directory: Unique identifier of the WorkOS Directory. This value can be obtained from the WorkOS dashboard or from the WorkOS API. + user: Unique identifier of the WorkOS Directory User. This value can be obtained from the WorkOS API. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[DirectoryGroup] + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "directory": directory, + "user": user, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path="directory_groups", + model=DirectoryGroup, + params=params, + request_options=request_options, + ) + + def get( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> DirectoryGroup: + """Get a Directory Group + + Get the details of an existing Directory Group. + + Args: + id: Unique identifier for the Directory Group. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + DirectoryGroup + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"directory_groups/{id}", + model=DirectoryGroup, + request_options=request_options, + ) + + find = get + + +class AsyncDirectoryGroups: + """Directory Groups API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[DirectoryGroupsOrder] = None, + directory: Optional[str] = None, + user: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[DirectoryGroup]: + """List Directory Groups + + Get a list of all of existing directory groups matching the criteria specified. + + Args: + directory: Unique identifier of the WorkOS Directory. This value can be obtained from the WorkOS dashboard or from the WorkOS API. + user: Unique identifier of the WorkOS Directory User. This value can be obtained from the WorkOS API. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[DirectoryGroup] + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "directory": directory, + "user": user, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path="directory_groups", + model=DirectoryGroup, + params=params, + request_options=request_options, + ) + + async def get( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> DirectoryGroup: + """Get a Directory Group + + Get the details of an existing Directory Group. + + Args: + id: Unique identifier for the Directory Group. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + DirectoryGroup + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"directory_groups/{id}", + model=DirectoryGroup, + request_options=request_options, + ) + + find = get diff --git a/src/workos/directory_groups/models/__init__.py b/src/workos/directory_groups/models/__init__.py new file mode 100644 index 00000000..12aece0f --- /dev/null +++ b/src/workos/directory_groups/models/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from .directory_group import DirectoryGroup as DirectoryGroup +from .directory_groups_order import DirectoryGroupsOrder as DirectoryGroupsOrder diff --git a/src/workos/directory_groups/models/directory_group.py b/src/workos/directory_groups/models/directory_group.py new file mode 100644 index 00000000..6b5fffa8 --- /dev/null +++ b/src/workos/directory_groups/models/directory_group.py @@ -0,0 +1,75 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class DirectoryGroup: + """Directory Group model.""" + + object: Literal["directory_group"] + """Distinguishes the Directory Group object.""" + id: str + """Unique identifier for the Directory Group.""" + idp_id: str + """Unique identifier for the group, assigned by the Directory Provider. Different Directory Providers use different ID formats.""" + directory_id: str + """The identifier of the Directory the Directory Group belongs to.""" + organization_id: str + """The identifier for the Organization in which the Directory resides.""" + name: str + """The name of the Directory Group.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + raw_attributes: Optional[Dict[str, Any]] = None + """The raw attributes received from the directory provider.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "DirectoryGroup": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + idp_id=data["idp_id"], + directory_id=data["directory_id"], + organization_id=data["organization_id"], + name=data["name"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + raw_attributes=data.get("raw_attributes"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing DirectoryGroup: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["idp_id"] = self.idp_id + result["directory_id"] = self.directory_id + result["organization_id"] = self.organization_id + result["name"] = self.name + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + if self.raw_attributes is not None: + result["raw_attributes"] = self.raw_attributes + return result diff --git a/src/workos/directory_groups/models/directory_groups_order.py b/src/workos/directory_groups/models/directory_groups_order.py new file mode 100644 index 00000000..9d62daab --- /dev/null +++ b/src/workos/directory_groups/models/directory_groups_order.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import ApplicationsOrder + +DirectoryGroupsOrder = ApplicationsOrder +__all__ = ["DirectoryGroupsOrder"] diff --git a/src/workos/directory_users/__init__.py b/src/workos/directory_users/__init__.py new file mode 100644 index 00000000..f8cbca8a --- /dev/null +++ b/src/workos/directory_users/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import DirectoryUsers, AsyncDirectoryUsers +from .models import * diff --git a/src/workos/directory_users/_resource.py b/src/workos/directory_users/_resource.py new file mode 100644 index 00000000..f97fea5b --- /dev/null +++ b/src/workos/directory_users/_resource.py @@ -0,0 +1,203 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import DirectoryUserWithGroups +from .models import DirectoryUsersOrder +from .._pagination import AsyncPage, SyncPage +from .._types import RequestOptions + + +class DirectoryUsers: + """Directory Users API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[DirectoryUsersOrder] = None, + directory: Optional[str] = None, + group: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[DirectoryUserWithGroups]: + """List Directory Users + + Get a list of all of existing Directory Users matching the criteria specified. + + Args: + directory: Unique identifier of the WorkOS Directory. This value can be obtained from the WorkOS dashboard or from the WorkOS API. + group: Unique identifier of the WorkOS Directory Group. This value can be obtained from the WorkOS API. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[DirectoryUserWithGroups] + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + RateLimitExceededException: If rate limited (429). + AuthenticationException: If the API key is invalid (401). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "directory": directory, + "group": group, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path="directory_users", + model=DirectoryUserWithGroups, + params=params, + request_options=request_options, + ) + + def get( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> DirectoryUserWithGroups: + """Get a Directory User + + Get the details of an existing Directory User. + + Args: + id: Unique identifier for the Directory User. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + DirectoryUserWithGroups + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"directory_users/{id}", + model=DirectoryUserWithGroups, + request_options=request_options, + ) + + find = get + + +class AsyncDirectoryUsers: + """Directory Users API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[DirectoryUsersOrder] = None, + directory: Optional[str] = None, + group: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[DirectoryUserWithGroups]: + """List Directory Users + + Get a list of all of existing Directory Users matching the criteria specified. + + Args: + directory: Unique identifier of the WorkOS Directory. This value can be obtained from the WorkOS dashboard or from the WorkOS API. + group: Unique identifier of the WorkOS Directory Group. This value can be obtained from the WorkOS API. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[DirectoryUserWithGroups] + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + RateLimitExceededException: If rate limited (429). + AuthenticationException: If the API key is invalid (401). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "directory": directory, + "group": group, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path="directory_users", + model=DirectoryUserWithGroups, + params=params, + request_options=request_options, + ) + + async def get( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> DirectoryUserWithGroups: + """Get a Directory User + + Get the details of an existing Directory User. + + Args: + id: Unique identifier for the Directory User. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + DirectoryUserWithGroups + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"directory_users/{id}", + model=DirectoryUserWithGroups, + request_options=request_options, + ) + + find = get diff --git a/src/workos/directory_users/models/__init__.py b/src/workos/directory_users/models/__init__.py new file mode 100644 index 00000000..893cfd57 --- /dev/null +++ b/src/workos/directory_users/models/__init__.py @@ -0,0 +1,9 @@ +# This file is auto-generated by oagen. Do not edit. + +from .directory_user_with_groups import ( + DirectoryUserWithGroups as DirectoryUserWithGroups, +) +from .directory_user_with_groups_email import ( + DirectoryUserWithGroupsEmail as DirectoryUserWithGroupsEmail, +) +from .directory_users_order import DirectoryUsersOrder as DirectoryUsersOrder diff --git a/src/workos/directory_users/models/directory_user_with_groups.py b/src/workos/directory_users/models/directory_user_with_groups.py new file mode 100644 index 00000000..4c9ea24f --- /dev/null +++ b/src/workos/directory_users/models/directory_user_with_groups.py @@ -0,0 +1,153 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import cast +from typing import Any, Dict, List, Literal, Optional +from workos._errors import BaseRequestException + +from workos.directory_groups.models import DirectoryGroup +from .directory_user_with_groups_email import DirectoryUserWithGroupsEmail +from workos.authorization.models import SlimRole +from workos.common.models import DirectoryUserWithGroupsState + + +@dataclass(slots=True) +class DirectoryUserWithGroups: + """Directory User With Groups model.""" + + object: Literal["directory_user"] + """Distinguishes the Directory User object.""" + id: str + """Unique identifier for the Directory User.""" + directory_id: str + """The identifier of the Directory the Directory User belongs to.""" + organization_id: str + """The identifier for the Organization in which the Directory resides.""" + idp_id: str + """Unique identifier for the user, assigned by the Directory Provider. Different Directory Providers use different ID formats.""" + email: Optional[str] + """The email address of the user.""" + state: "DirectoryUserWithGroupsState" + """The state of the user.""" + raw_attributes: Dict[str, Any] + """The raw attributes received from the directory provider.""" + custom_attributes: Dict[str, Any] + """An object containing the custom attribute mapping for the Directory Provider.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + groups: List["DirectoryGroup"] + """The directory groups the user belongs to.""" + first_name: Optional[str] = None + """The first name of the user.""" + last_name: Optional[str] = None + """The last name of the user.""" + emails: Optional[List["DirectoryUserWithGroupsEmail"]] = None + """A list of email addresses for the user.""" + job_title: Optional[str] = None + """The job title of the user.""" + username: Optional[str] = None + """The username of the user.""" + role: Optional["SlimRole"] = None + """The primary role assigned to the user.""" + roles: Optional[List["SlimRole"]] = None + """All roles assigned to the user.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "DirectoryUserWithGroups": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + directory_id=data["directory_id"], + organization_id=data["organization_id"], + idp_id=data["idp_id"], + email=data["email"], + state=DirectoryUserWithGroupsState(data["state"]), + raw_attributes=data["raw_attributes"], + custom_attributes=data["custom_attributes"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + groups=[ + DirectoryGroup.from_dict(cast(Dict[str, Any], item)) + for item in cast(list[Any], data["groups"]) + ], + first_name=data.get("first_name"), + last_name=data.get("last_name"), + emails=[ + DirectoryUserWithGroupsEmail.from_dict(cast(Dict[str, Any], item)) + for item in cast(list[Any], _v) + ] + if (_v := data.get("emails")) is not None + else None, + job_title=data.get("job_title"), + username=data.get("username"), + role=SlimRole.from_dict(cast(Dict[str, Any], _v)) + if (_v := data.get("role")) is not None + else None, + roles=[ + SlimRole.from_dict(cast(Dict[str, Any], item)) + for item in cast(list[Any], _v) + ] + if (_v := data.get("roles")) is not None + else None, + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing DirectoryUserWithGroups: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["directory_id"] = self.directory_id + result["organization_id"] = self.organization_id + result["idp_id"] = self.idp_id + if self.email is not None: + result["email"] = self.email + else: + result["email"] = None + result["state"] = self.state + result["raw_attributes"] = self.raw_attributes + result["custom_attributes"] = self.custom_attributes + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["groups"] = [item.to_dict() for item in self.groups] + if self.first_name is not None: + result["first_name"] = self.first_name + else: + result["first_name"] = None + if self.last_name is not None: + result["last_name"] = self.last_name + else: + result["last_name"] = None + if self.emails is not None: + result["emails"] = [item.to_dict() for item in self.emails] + if self.job_title is not None: + result["job_title"] = self.job_title + else: + result["job_title"] = None + if self.username is not None: + result["username"] = self.username + else: + result["username"] = None + if self.role is not None: + result["role"] = self.role.to_dict() + if self.roles is not None: + result["roles"] = [item.to_dict() for item in self.roles] + return result diff --git a/src/workos/directory_users/models/directory_user_with_groups_email.py b/src/workos/directory_users/models/directory_user_with_groups_email.py new file mode 100644 index 00000000..1b8aa00a --- /dev/null +++ b/src/workos/directory_users/models/directory_user_with_groups_email.py @@ -0,0 +1,46 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class DirectoryUserWithGroupsEmail: + """Directory User With Groups Email model.""" + + primary: Optional[bool] = None + """Whether this is the primary email address.""" + type: Optional[str] = None + """The type of email address.""" + value: Optional[str] = None + """The email address value.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "DirectoryUserWithGroupsEmail": + """Deserialize from a dictionary.""" + try: + return cls( + primary=data.get("primary"), + type=data.get("type"), + value=data.get("value"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing DirectoryUserWithGroupsEmail: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + if self.primary is not None: + result["primary"] = self.primary + if self.type is not None: + result["type"] = self.type + if self.value is not None: + result["value"] = self.value + else: + result["value"] = None + return result diff --git a/src/workos/directory_users/models/directory_users_order.py b/src/workos/directory_users/models/directory_users_order.py new file mode 100644 index 00000000..161320cd --- /dev/null +++ b/src/workos/directory_users/models/directory_users_order.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import ApplicationsOrder + +DirectoryUsersOrder = ApplicationsOrder +__all__ = ["DirectoryUsersOrder"] diff --git a/src/workos/events/__init__.py b/src/workos/events/__init__.py new file mode 100644 index 00000000..3561f7bb --- /dev/null +++ b/src/workos/events/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import Events, AsyncEvents +from .models import * diff --git a/src/workos/events/_resource.py b/src/workos/events/_resource.py new file mode 100644 index 00000000..1d7203b2 --- /dev/null +++ b/src/workos/events/_resource.py @@ -0,0 +1,151 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, List, Optional + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import Event +from .models import EventsOrder +from .._pagination import AsyncPage, SyncPage +from .._types import RequestOptions + + +class Events: + """Events API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def list_events( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[EventsOrder] = None, + events: Optional[List[str]] = None, + range_start: Optional[str] = None, + range_end: Optional[str] = None, + organization_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[Event]: + """List events + + List events for the current environment. + + Args: + events: Filter events by one or more event types (e.g. `dsync.user.created`). + range_start: ISO-8601 date string to filter events created after this date. + range_end: ISO-8601 date string to filter events created before this date. + organization_id: Filter events by the [Organization](https://workos.com/docs/reference/organization) that the event is associated with. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[Event] + + Raises: + BadRequestException: If the request is malformed (400). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "events": events, + "range_start": range_start, + "range_end": range_end, + "organization_id": organization_id, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path="events", + model=Event, + params=params, + request_options=request_options, + ) + + list = list_events + + +class AsyncEvents: + """Events API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def list_events( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[EventsOrder] = None, + events: Optional[List[str]] = None, + range_start: Optional[str] = None, + range_end: Optional[str] = None, + organization_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[Event]: + """List events + + List events for the current environment. + + Args: + events: Filter events by one or more event types (e.g. `dsync.user.created`). + range_start: ISO-8601 date string to filter events created after this date. + range_end: ISO-8601 date string to filter events created before this date. + organization_id: Filter events by the [Organization](https://workos.com/docs/reference/organization) that the event is associated with. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[Event] + + Raises: + BadRequestException: If the request is malformed (400). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "events": events, + "range_start": range_start, + "range_end": range_end, + "organization_id": organization_id, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path="events", + model=Event, + params=params, + request_options=request_options, + ) + + list = list_events diff --git a/src/workos/events/models/__init__.py b/src/workos/events/models/__init__.py new file mode 100644 index 00000000..0116ecb9 --- /dev/null +++ b/src/workos/events/models/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from .event import Event as Event +from .events_order import EventsOrder as EventsOrder diff --git a/src/workos/events/models/event.py b/src/workos/events/models/event.py new file mode 100644 index 00000000..d0640e4e --- /dev/null +++ b/src/workos/events/models/event.py @@ -0,0 +1,59 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class Event: + """Event model.""" + + object: Literal["event"] + """Distinguishes the Event object.""" + id: str + """Unique identifier for the Event.""" + event: str + """The type of event that occurred.""" + data: Dict[str, Any] + """The event payload.""" + created_at: datetime + """An ISO 8601 timestamp.""" + context: Optional[Dict[str, Any]] = None + """Additional context about the event.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Event": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + event=data["event"], + data=data["data"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + context=data.get("context"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing Event: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["event"] = self.event + result["data"] = self.data + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + if self.context is not None: + result["context"] = self.context + return result diff --git a/src/workos/events/models/events_order.py b/src/workos/events/models/events_order.py new file mode 100644 index 00000000..85eb8611 --- /dev/null +++ b/src/workos/events/models/events_order.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import ApplicationsOrder + +EventsOrder = ApplicationsOrder +__all__ = ["EventsOrder"] diff --git a/src/workos/exceptions.py b/src/workos/exceptions.py new file mode 100644 index 00000000..84a4b6d1 --- /dev/null +++ b/src/workos/exceptions.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._errors import * # noqa: F401,F403 diff --git a/src/workos/feature_flags/__init__.py b/src/workos/feature_flags/__init__.py new file mode 100644 index 00000000..573190b2 --- /dev/null +++ b/src/workos/feature_flags/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import FeatureFlags, AsyncFeatureFlags +from .models import * diff --git a/src/workos/feature_flags/_resource.py b/src/workos/feature_flags/_resource.py new file mode 100644 index 00000000..589ee571 --- /dev/null +++ b/src/workos/feature_flags/_resource.py @@ -0,0 +1,309 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import FeatureFlag, Flag +from .models import FeatureFlagsOrder +from .._pagination import AsyncPage, SyncPage +from .._types import RequestOptions + + +class FeatureFlags: + """Feature Flags API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[FeatureFlagsOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[Flag]: + """List feature flags + + Get a list of all of your existing feature flags matching the criteria specified. + + Args: + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[Flag] + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path="feature-flags", + model=Flag, + params=params, + request_options=request_options, + ) + + def get_by_slug( + self, + slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Flag: + """Get a feature flag + + Get the details of an existing feature flag by its slug. + + Args: + slug: A unique key to reference the Feature Flag. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Flag + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"feature-flags/{slug}", + model=Flag, + request_options=request_options, + ) + + find_by_slug = get_by_slug + + def disable_flag( + self, + slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> FeatureFlag: + """Disable a feature flag + + Disables a feature flag in the current environment. + + Args: + slug: A unique key to reference the Feature Flag. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + FeatureFlag + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="put", + path=f"feature-flags/{slug}/disable", + model=FeatureFlag, + request_options=request_options, + ) + + def enable_flag( + self, + slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> FeatureFlag: + """Enable a feature flag + + Enables a feature flag in the current environment. + + Args: + slug: A unique key to reference the Feature Flag. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + FeatureFlag + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="put", + path=f"feature-flags/{slug}/enable", + model=FeatureFlag, + request_options=request_options, + ) + + +class AsyncFeatureFlags: + """Feature Flags API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[FeatureFlagsOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[Flag]: + """List feature flags + + Get a list of all of your existing feature flags matching the criteria specified. + + Args: + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[Flag] + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path="feature-flags", + model=Flag, + params=params, + request_options=request_options, + ) + + async def get_by_slug( + self, + slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Flag: + """Get a feature flag + + Get the details of an existing feature flag by its slug. + + Args: + slug: A unique key to reference the Feature Flag. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Flag + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"feature-flags/{slug}", + model=Flag, + request_options=request_options, + ) + + find_by_slug = get_by_slug + + async def disable_flag( + self, + slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> FeatureFlag: + """Disable a feature flag + + Disables a feature flag in the current environment. + + Args: + slug: A unique key to reference the Feature Flag. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + FeatureFlag + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="put", + path=f"feature-flags/{slug}/disable", + model=FeatureFlag, + request_options=request_options, + ) + + async def enable_flag( + self, + slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> FeatureFlag: + """Enable a feature flag + + Enables a feature flag in the current environment. + + Args: + slug: A unique key to reference the Feature Flag. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + FeatureFlag + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="put", + path=f"feature-flags/{slug}/enable", + model=FeatureFlag, + request_options=request_options, + ) diff --git a/src/workos/feature_flags/models/__init__.py b/src/workos/feature_flags/models/__init__.py new file mode 100644 index 00000000..073bb44f --- /dev/null +++ b/src/workos/feature_flags/models/__init__.py @@ -0,0 +1,7 @@ +# This file is auto-generated by oagen. Do not edit. + +from .feature_flag import FeatureFlag as FeatureFlag +from .feature_flag_owner import FeatureFlagOwner as FeatureFlagOwner +from .feature_flags_order import FeatureFlagsOrder as FeatureFlagsOrder +from .flag import Flag as Flag +from .flag_owner import FlagOwner as FlagOwner diff --git a/src/workos/feature_flags/models/feature_flag.py b/src/workos/feature_flags/models/feature_flag.py new file mode 100644 index 00000000..b8409c53 --- /dev/null +++ b/src/workos/feature_flags/models/feature_flag.py @@ -0,0 +1,93 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import cast +from typing import Any, Dict, List, Literal, Optional +from workos._errors import BaseRequestException + +from .feature_flag_owner import FeatureFlagOwner + + +@dataclass(slots=True) +class FeatureFlag: + """Feature Flag model.""" + + object: Literal["feature_flag"] + """Distinguishes the Feature Flag object.""" + id: str + """Unique identifier of the Feature Flag.""" + slug: str + """A unique key to reference the Feature Flag.""" + name: str + """A descriptive name for the Feature Flag. This field does not need to be unique.""" + description: Optional[str] + """A description for the Feature Flag.""" + owner: Optional["FeatureFlagOwner"] + """The owner of the Feature Flag.""" + tags: List[str] + """Labels assigned to the Feature Flag for categorizing and filtering.""" + enabled: bool + """Specifies whether the Feature Flag is active for the current environment.""" + default_value: bool + """The value returned for users and organizations who don't match any configured targeting rules.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "FeatureFlag": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + slug=data["slug"], + name=data["name"], + description=data["description"], + owner=FeatureFlagOwner.from_dict(cast(Dict[str, Any], _v)) + if (_v := data["owner"]) is not None + else None, + tags=data["tags"], + enabled=data["enabled"], + default_value=data["default_value"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing FeatureFlag: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["slug"] = self.slug + result["name"] = self.name + if self.description is not None: + result["description"] = self.description + else: + result["description"] = None + if self.owner is not None: + result["owner"] = self.owner.to_dict() + else: + result["owner"] = None + result["tags"] = self.tags + result["enabled"] = self.enabled + result["default_value"] = self.default_value + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + return result diff --git a/src/workos/feature_flags/models/feature_flag_owner.py b/src/workos/feature_flags/models/feature_flag_owner.py new file mode 100644 index 00000000..99cf1f9a --- /dev/null +++ b/src/workos/feature_flags/models/feature_flag_owner.py @@ -0,0 +1,47 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class FeatureFlagOwner: + """Feature Flag Owner model.""" + + email: str + """The email address of the flag owner.""" + first_name: Optional[str] + """The first name of the flag owner.""" + last_name: Optional[str] + """The last name of the flag owner.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "FeatureFlagOwner": + """Deserialize from a dictionary.""" + try: + return cls( + email=data["email"], + first_name=data["first_name"], + last_name=data["last_name"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing FeatureFlagOwner: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["email"] = self.email + if self.first_name is not None: + result["first_name"] = self.first_name + else: + result["first_name"] = None + if self.last_name is not None: + result["last_name"] = self.last_name + else: + result["last_name"] = None + return result diff --git a/src/workos/feature_flags/models/feature_flags_order.py b/src/workos/feature_flags/models/feature_flags_order.py new file mode 100644 index 00000000..94283a01 --- /dev/null +++ b/src/workos/feature_flags/models/feature_flags_order.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import ApplicationsOrder + +FeatureFlagsOrder = ApplicationsOrder +__all__ = ["FeatureFlagsOrder"] diff --git a/src/workos/feature_flags/models/flag.py b/src/workos/feature_flags/models/flag.py new file mode 100644 index 00000000..dd56a103 --- /dev/null +++ b/src/workos/feature_flags/models/flag.py @@ -0,0 +1,93 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import cast +from typing import Any, Dict, List, Literal, Optional +from workos._errors import BaseRequestException + +from .flag_owner import FlagOwner + + +@dataclass(slots=True) +class Flag: + """Flag model.""" + + object: Literal["feature_flag"] + """Distinguishes the Feature Flag object.""" + id: str + """Unique identifier of the Feature Flag.""" + slug: str + """A unique key to reference the Feature Flag.""" + name: str + """A descriptive name for the Feature Flag. This field does not need to be unique.""" + description: Optional[str] + """A description for the Feature Flag.""" + owner: Optional["FlagOwner"] + """The owner of the Feature Flag.""" + tags: List[str] + """Labels assigned to the Feature Flag for categorizing and filtering.""" + enabled: bool + """Specifies whether the Feature Flag is active for the current environment.""" + default_value: bool + """The value returned for users and organizations who don't match any configured targeting rules.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Flag": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + slug=data["slug"], + name=data["name"], + description=data["description"], + owner=FlagOwner.from_dict(cast(Dict[str, Any], _v)) + if (_v := data["owner"]) is not None + else None, + tags=data["tags"], + enabled=data["enabled"], + default_value=data["default_value"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing Flag: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["slug"] = self.slug + result["name"] = self.name + if self.description is not None: + result["description"] = self.description + else: + result["description"] = None + if self.owner is not None: + result["owner"] = self.owner.to_dict() + else: + result["owner"] = None + result["tags"] = self.tags + result["enabled"] = self.enabled + result["default_value"] = self.default_value + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + return result diff --git a/src/workos/feature_flags/models/flag_owner.py b/src/workos/feature_flags/models/flag_owner.py new file mode 100644 index 00000000..e3d92c1a --- /dev/null +++ b/src/workos/feature_flags/models/flag_owner.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from .feature_flag_owner import FeatureFlagOwner + +FlagOwner = FeatureFlagOwner diff --git a/src/workos/feature_flags/targets/__init__.py b/src/workos/feature_flags/targets/__init__.py new file mode 100644 index 00000000..929c6da2 --- /dev/null +++ b/src/workos/feature_flags/targets/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import FeatureFlagsTargets, AsyncFeatureFlagsTargets +from .models import * diff --git a/src/workos/feature_flags/targets/_resource.py b/src/workos/feature_flags/targets/_resource.py new file mode 100644 index 00000000..fd90b442 --- /dev/null +++ b/src/workos/feature_flags/targets/_resource.py @@ -0,0 +1,144 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from ..._client import AsyncWorkOSClient, WorkOSClient + +from ..._types import RequestOptions + + +class FeatureFlagsTargets: + """Feature Flags Targets API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def create_target( + self, + resource_id: str, + slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Add a feature flag target + + Enables a feature flag for a specific target in the current environment. Currently, supported targets include users and organizations. + + Args: + resource_id: The resource ID in format "user_" or "org_". + slug: The unique slug identifier of the feature flag. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + self._client.request( + method="post", + path=f"feature-flags/{slug}/targets/{resource_id}", + request_options=request_options, + ) + + def delete_target( + self, + resource_id: str, + slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Remove a feature flag target + + Removes a target from the feature flag's target list in the current environment. Currently, supported targets include users and organizations. + + Args: + resource_id: The resource ID in format "user_" or "org_". + slug: The unique slug identifier of the feature flag. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + self._client.request( + method="delete", + path=f"feature-flags/{slug}/targets/{resource_id}", + request_options=request_options, + ) + + +class AsyncFeatureFlagsTargets: + """Feature Flags Targets API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def create_target( + self, + resource_id: str, + slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Add a feature flag target + + Enables a feature flag for a specific target in the current environment. Currently, supported targets include users and organizations. + + Args: + resource_id: The resource ID in format "user_" or "org_". + slug: The unique slug identifier of the feature flag. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + await self._client.request( + method="post", + path=f"feature-flags/{slug}/targets/{resource_id}", + request_options=request_options, + ) + + async def delete_target( + self, + resource_id: str, + slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Remove a feature flag target + + Removes a target from the feature flag's target list in the current environment. Currently, supported targets include users and organizations. + + Args: + resource_id: The resource ID in format "user_" or "org_". + slug: The unique slug identifier of the feature flag. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + await self._client.request( + method="delete", + path=f"feature-flags/{slug}/targets/{resource_id}", + request_options=request_options, + ) diff --git a/src/workos/feature_flags/targets/models/__init__.py b/src/workos/feature_flags/targets/models/__init__.py new file mode 100644 index 00000000..33e9c7b7 --- /dev/null +++ b/src/workos/feature_flags/targets/models/__init__.py @@ -0,0 +1,2 @@ +# This file is auto-generated by oagen. Do not edit. + diff --git a/src/workos/multi_factor_auth/__init__.py b/src/workos/multi_factor_auth/__init__.py new file mode 100644 index 00000000..0154d5af --- /dev/null +++ b/src/workos/multi_factor_auth/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import MultiFactorAuth, AsyncMultiFactorAuth +from .models import * diff --git a/src/workos/multi_factor_auth/_resource.py b/src/workos/multi_factor_auth/_resource.py new file mode 100644 index 00000000..3d27d8c9 --- /dev/null +++ b/src/workos/multi_factor_auth/_resource.py @@ -0,0 +1,321 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Optional + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import AuthenticationFactor, AuthenticationFactorEnrolled +from workos.multi_factor_auth.challenges.models import AuthenticationChallenge +from workos.common.models import AuthenticationFactorsCreateRequestType +from .._types import RequestOptions + + +class MultiFactorAuth: + """Multi Factor Auth API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def create( + self, + *, + type: AuthenticationFactorsCreateRequestType, + phone_number: Optional[str] = None, + totp_issuer: Optional[str] = None, + totp_user: Optional[str] = None, + user_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AuthenticationFactorEnrolled: + """Enroll Factor + + Enrolls an Authentication Factor to be used as an additional factor of authentication. The returned ID should be used to create an authentication Challenge. + + Args: + type: The type of factor to enroll. + phone_number: Required when type is 'sms'. + totp_issuer: Required when type is 'totp'. + totp_user: Required when type is 'totp'. + user_id: The ID of the user to associate the factor with. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthenticationFactorEnrolled + + Raises: + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "type": type, + "phone_number": phone_number, + "totp_issuer": totp_issuer, + "totp_user": totp_user, + "user_id": user_id, + }.items() + if v is not None + } + return self._client.request( + method="post", + path="auth/factors/enroll", + body=body, + model=AuthenticationFactorEnrolled, + request_options=request_options, + ) + + def get( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> AuthenticationFactor: + """Get Factor + + Gets an Authentication Factor. + + Args: + id: The unique ID of the Factor. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthenticationFactor + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"auth/factors/{id}", + model=AuthenticationFactor, + request_options=request_options, + ) + + def delete( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete Factor + + Permanently deletes an Authentication Factor. It cannot be undone. + + Args: + id: The unique ID of the Factor. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + self._client.request( + method="delete", + path=f"auth/factors/{id}", + request_options=request_options, + ) + + def challenge( + self, + id: str, + *, + sms_template: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AuthenticationChallenge: + """Challenge Factor + + Creates a Challenge for an Authentication Factor. + + Args: + id: The unique ID of the Authentication Factor to be challenged. + sms_template: A custom template for the SMS message. Use the {{code}} placeholder to include the verification code. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthenticationChallenge + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "sms_template": sms_template, + }.items() + if v is not None + } + return self._client.request( + method="post", + path=f"auth/factors/{id}/challenge", + body=body, + model=AuthenticationChallenge, + request_options=request_options, + ) + + +class AsyncMultiFactorAuth: + """Multi Factor Auth API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def create( + self, + *, + type: AuthenticationFactorsCreateRequestType, + phone_number: Optional[str] = None, + totp_issuer: Optional[str] = None, + totp_user: Optional[str] = None, + user_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AuthenticationFactorEnrolled: + """Enroll Factor + + Enrolls an Authentication Factor to be used as an additional factor of authentication. The returned ID should be used to create an authentication Challenge. + + Args: + type: The type of factor to enroll. + phone_number: Required when type is 'sms'. + totp_issuer: Required when type is 'totp'. + totp_user: Required when type is 'totp'. + user_id: The ID of the user to associate the factor with. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthenticationFactorEnrolled + + Raises: + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "type": type, + "phone_number": phone_number, + "totp_issuer": totp_issuer, + "totp_user": totp_user, + "user_id": user_id, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path="auth/factors/enroll", + body=body, + model=AuthenticationFactorEnrolled, + request_options=request_options, + ) + + async def get( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> AuthenticationFactor: + """Get Factor + + Gets an Authentication Factor. + + Args: + id: The unique ID of the Factor. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthenticationFactor + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"auth/factors/{id}", + model=AuthenticationFactor, + request_options=request_options, + ) + + async def delete( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete Factor + + Permanently deletes an Authentication Factor. It cannot be undone. + + Args: + id: The unique ID of the Factor. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + await self._client.request( + method="delete", + path=f"auth/factors/{id}", + request_options=request_options, + ) + + async def challenge( + self, + id: str, + *, + sms_template: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AuthenticationChallenge: + """Challenge Factor + + Creates a Challenge for an Authentication Factor. + + Args: + id: The unique ID of the Authentication Factor to be challenged. + sms_template: A custom template for the SMS message. Use the {{code}} placeholder to include the verification code. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthenticationChallenge + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "sms_template": sms_template, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path=f"auth/factors/{id}/challenge", + body=body, + model=AuthenticationChallenge, + request_options=request_options, + ) diff --git a/src/workos/multi_factor_auth/challenges/__init__.py b/src/workos/multi_factor_auth/challenges/__init__.py new file mode 100644 index 00000000..01488ceb --- /dev/null +++ b/src/workos/multi_factor_auth/challenges/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import MultiFactorAuthChallenges, AsyncMultiFactorAuthChallenges +from .models import * diff --git a/src/workos/multi_factor_auth/challenges/_resource.py b/src/workos/multi_factor_auth/challenges/_resource.py new file mode 100644 index 00000000..06d149ae --- /dev/null +++ b/src/workos/multi_factor_auth/challenges/_resource.py @@ -0,0 +1,101 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Optional + +if TYPE_CHECKING: + from ..._client import AsyncWorkOSClient, WorkOSClient + +from .models import AuthenticationChallengeVerifyResponse +from ..._types import RequestOptions + + +class MultiFactorAuthChallenges: + """Multi Factor Auth Challenges API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def verify( + self, + id: str, + *, + code: str, + request_options: Optional[RequestOptions] = None, + ) -> AuthenticationChallengeVerifyResponse: + """Verify Challenge + + Verifies an Authentication Challenge. + + Args: + id: The unique ID of the Authentication Challenge. + code: The one-time code to verify. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthenticationChallengeVerifyResponse + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "code": code, + } + return self._client.request( + method="post", + path=f"auth/challenges/{id}/verify", + body=body, + model=AuthenticationChallengeVerifyResponse, + request_options=request_options, + ) + + +class AsyncMultiFactorAuthChallenges: + """Multi Factor Auth Challenges API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def verify( + self, + id: str, + *, + code: str, + request_options: Optional[RequestOptions] = None, + ) -> AuthenticationChallengeVerifyResponse: + """Verify Challenge + + Verifies an Authentication Challenge. + + Args: + id: The unique ID of the Authentication Challenge. + code: The one-time code to verify. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthenticationChallengeVerifyResponse + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "code": code, + } + return await self._client.request( + method="post", + path=f"auth/challenges/{id}/verify", + body=body, + model=AuthenticationChallengeVerifyResponse, + request_options=request_options, + ) diff --git a/src/workos/multi_factor_auth/challenges/models/__init__.py b/src/workos/multi_factor_auth/challenges/models/__init__.py new file mode 100644 index 00000000..67bef27b --- /dev/null +++ b/src/workos/multi_factor_auth/challenges/models/__init__.py @@ -0,0 +1,9 @@ +# This file is auto-generated by oagen. Do not edit. + +from .authentication_challenge import AuthenticationChallenge as AuthenticationChallenge +from .authentication_challenge_verify_response import ( + AuthenticationChallengeVerifyResponse as AuthenticationChallengeVerifyResponse, +) +from .authentication_challenges_verify_request import ( + AuthenticationChallengesVerifyRequest as AuthenticationChallengesVerifyRequest, +) diff --git a/src/workos/multi_factor_auth/challenges/models/authentication_challenge.py b/src/workos/multi_factor_auth/challenges/models/authentication_challenge.py new file mode 100644 index 00000000..370fede1 --- /dev/null +++ b/src/workos/multi_factor_auth/challenges/models/authentication_challenge.py @@ -0,0 +1,72 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AuthenticationChallenge: + """The authentication challenge object.""" + + object: Literal["authentication_challenge"] + """Distinguishes the authentication challenge object.""" + id: str + """The unique ID of the authentication challenge.""" + authentication_factor_id: str + """The unique ID of the authentication factor the challenge belongs to.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + expires_at: Optional[datetime] = None + """The timestamp when the challenge will expire. Does not apply to TOTP factors.""" + code: Optional[str] = None + """The one-time code for the challenge.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuthenticationChallenge": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + authentication_factor_id=data["authentication_factor_id"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + expires_at=datetime.fromisoformat(_v.replace("Z", "+00:00")) + if (_v := data.get("expires_at")) is not None + else None, + code=data.get("code"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuthenticationChallenge: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["authentication_factor_id"] = self.authentication_factor_id + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + if self.expires_at is not None: + result["expires_at"] = self.expires_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + if self.code is not None: + result["code"] = self.code + return result diff --git a/src/workos/multi_factor_auth/challenges/models/authentication_challenge_verify_response.py b/src/workos/multi_factor_auth/challenges/models/authentication_challenge_verify_response.py new file mode 100644 index 00000000..756e57bf --- /dev/null +++ b/src/workos/multi_factor_auth/challenges/models/authentication_challenge_verify_response.py @@ -0,0 +1,41 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict +from workos._errors import BaseRequestException + +from .authentication_challenge import AuthenticationChallenge + + +@dataclass(slots=True) +class AuthenticationChallengeVerifyResponse: + """Authentication Challenge Verify Response model.""" + + challenge: "AuthenticationChallenge" + valid: bool + """Whether the code was valid.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuthenticationChallengeVerifyResponse": + """Deserialize from a dictionary.""" + try: + return cls( + challenge=AuthenticationChallenge.from_dict( + cast(Dict[str, Any], data["challenge"]) + ), + valid=data["valid"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuthenticationChallengeVerifyResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["challenge"] = self.challenge.to_dict() + result["valid"] = self.valid + return result diff --git a/src/workos/multi_factor_auth/challenges/models/authentication_challenges_verify_request.py b/src/workos/multi_factor_auth/challenges/models/authentication_challenges_verify_request.py new file mode 100644 index 00000000..8ea9ef27 --- /dev/null +++ b/src/workos/multi_factor_auth/challenges/models/authentication_challenges_verify_request.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AuthenticationChallengesVerifyRequest: + """Authentication Challenges Verify Request model.""" + + code: str + """The one-time code to verify.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuthenticationChallengesVerifyRequest": + """Deserialize from a dictionary.""" + try: + return cls( + code=data["code"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuthenticationChallengesVerifyRequest: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["code"] = self.code + return result diff --git a/src/workos/multi_factor_auth/models/__init__.py b/src/workos/multi_factor_auth/models/__init__.py new file mode 100644 index 00000000..dd09ed7d --- /dev/null +++ b/src/workos/multi_factor_auth/models/__init__.py @@ -0,0 +1,24 @@ +# This file is auto-generated by oagen. Do not edit. + +from .authentication_factor import AuthenticationFactor as AuthenticationFactor +from .authentication_factor_enrolled import ( + AuthenticationFactorEnrolled as AuthenticationFactorEnrolled, +) +from .authentication_factor_enrolled_sms import ( + AuthenticationFactorEnrolledSms as AuthenticationFactorEnrolledSms, +) +from .authentication_factor_enrolled_totp import ( + AuthenticationFactorEnrolledTotp as AuthenticationFactorEnrolledTotp, +) +from .authentication_factor_sms import ( + AuthenticationFactorSms as AuthenticationFactorSms, +) +from .authentication_factor_totp import ( + AuthenticationFactorTotp as AuthenticationFactorTotp, +) +from .authentication_factors_create_request import ( + AuthenticationFactorsCreateRequest as AuthenticationFactorsCreateRequest, +) +from .challenge_authentication_factor import ( + ChallengeAuthenticationFactor as ChallengeAuthenticationFactor, +) diff --git a/src/workos/multi_factor_auth/models/authentication_factor.py b/src/workos/multi_factor_auth/models/authentication_factor.py new file mode 100644 index 00000000..91c3ed23 --- /dev/null +++ b/src/workos/multi_factor_auth/models/authentication_factor.py @@ -0,0 +1,82 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import cast +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + +from .authentication_factor_sms import AuthenticationFactorSms +from .authentication_factor_totp import AuthenticationFactorTotp +from workos.common.models import AuthenticationFactorType + + +@dataclass(slots=True) +class AuthenticationFactor: + """Authentication Factor model.""" + + object: Literal["authentication_factor"] + """Distinguishes the authentication factor object.""" + id: str + """The unique ID of the factor.""" + type: "AuthenticationFactorType" + """The type of the factor to enroll.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + user_id: Optional[str] = None + """The ID of the [user](https://workos.com/docs/reference/authkit/user).""" + sms: Optional["AuthenticationFactorSms"] = None + """SMS-based authentication factor details.""" + totp: Optional["AuthenticationFactorTotp"] = None + """TOTP-based authentication factor details.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuthenticationFactor": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + type=AuthenticationFactorType(data["type"]), + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + user_id=data.get("user_id"), + sms=AuthenticationFactorSms.from_dict(cast(Dict[str, Any], _v)) + if (_v := data.get("sms")) is not None + else None, + totp=AuthenticationFactorTotp.from_dict(cast(Dict[str, Any], _v)) + if (_v := data.get("totp")) is not None + else None, + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuthenticationFactor: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["type"] = self.type + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + if self.user_id is not None: + result["user_id"] = self.user_id + if self.sms is not None: + result["sms"] = self.sms.to_dict() + if self.totp is not None: + result["totp"] = self.totp.to_dict() + return result diff --git a/src/workos/multi_factor_auth/models/authentication_factor_enrolled.py b/src/workos/multi_factor_auth/models/authentication_factor_enrolled.py new file mode 100644 index 00000000..175bee8e --- /dev/null +++ b/src/workos/multi_factor_auth/models/authentication_factor_enrolled.py @@ -0,0 +1,84 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import cast +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + +from .authentication_factor_enrolled_sms import AuthenticationFactorEnrolledSms +from .authentication_factor_enrolled_totp import AuthenticationFactorEnrolledTotp +from workos.common.models import AuthenticationFactorEnrolledType + + +@dataclass(slots=True) +class AuthenticationFactorEnrolled: + """The [authentication factor](https://workos.com/docs/reference/authkit/mfa/authentication-factor) object that represents the additional authentication method used on top of the existing authentication strategy.""" + + object: Literal["authentication_factor"] + """Distinguishes the authentication factor object.""" + id: str + """The unique ID of the factor.""" + type: "AuthenticationFactorEnrolledType" + """The type of the factor to enroll.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + user_id: Optional[str] = None + """The ID of the [user](https://workos.com/docs/reference/authkit/user).""" + sms: Optional["AuthenticationFactorEnrolledSms"] = None + """SMS-based authentication factor details.""" + totp: Optional["AuthenticationFactorEnrolledTotp"] = None + """TOTP-based authentication factor details. Includes enrollment secrets only available at creation time.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuthenticationFactorEnrolled": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + type=AuthenticationFactorEnrolledType(data["type"]), + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + user_id=data.get("user_id"), + sms=AuthenticationFactorEnrolledSms.from_dict(cast(Dict[str, Any], _v)) + if (_v := data.get("sms")) is not None + else None, + totp=AuthenticationFactorEnrolledTotp.from_dict( + cast(Dict[str, Any], _v) + ) + if (_v := data.get("totp")) is not None + else None, + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuthenticationFactorEnrolled: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["type"] = self.type + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + if self.user_id is not None: + result["user_id"] = self.user_id + if self.sms is not None: + result["sms"] = self.sms.to_dict() + if self.totp is not None: + result["totp"] = self.totp.to_dict() + return result diff --git a/src/workos/multi_factor_auth/models/authentication_factor_enrolled_sms.py b/src/workos/multi_factor_auth/models/authentication_factor_enrolled_sms.py new file mode 100644 index 00000000..d3559405 --- /dev/null +++ b/src/workos/multi_factor_auth/models/authentication_factor_enrolled_sms.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AuthenticationFactorEnrolledSms: + """SMS-based authentication factor details.""" + + phone_number: str + """The user's phone number for SMS-based authentication.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuthenticationFactorEnrolledSms": + """Deserialize from a dictionary.""" + try: + return cls( + phone_number=data["phone_number"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuthenticationFactorEnrolledSms: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["phone_number"] = self.phone_number + return result diff --git a/src/workos/multi_factor_auth/models/authentication_factor_enrolled_totp.py b/src/workos/multi_factor_auth/models/authentication_factor_enrolled_totp.py new file mode 100644 index 00000000..9973368d --- /dev/null +++ b/src/workos/multi_factor_auth/models/authentication_factor_enrolled_totp.py @@ -0,0 +1,49 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AuthenticationFactorEnrolledTotp: + """TOTP-based authentication factor details. Includes enrollment secrets only available at creation time.""" + + issuer: str + """Your application or company name displayed in the user's authenticator app. Defaults to your WorkOS team name.""" + user: str + """The user's account name displayed in their authenticator app. Defaults to the user's email.""" + secret: str + """TOTP secret that can be manually entered into some authenticator apps in place of scanning a QR code.""" + qr_code: str + """Base64 encoded image containing scannable QR code.""" + uri: str + """The `otpauth` URI that is encoded by the provided `qr_code`.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuthenticationFactorEnrolledTotp": + """Deserialize from a dictionary.""" + try: + return cls( + issuer=data["issuer"], + user=data["user"], + secret=data["secret"], + qr_code=data["qr_code"], + uri=data["uri"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuthenticationFactorEnrolledTotp: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["issuer"] = self.issuer + result["user"] = self.user + result["secret"] = self.secret + result["qr_code"] = self.qr_code + result["uri"] = self.uri + return result diff --git a/src/workos/multi_factor_auth/models/authentication_factor_sms.py b/src/workos/multi_factor_auth/models/authentication_factor_sms.py new file mode 100644 index 00000000..2c845387 --- /dev/null +++ b/src/workos/multi_factor_auth/models/authentication_factor_sms.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from .authentication_factor_enrolled_sms import AuthenticationFactorEnrolledSms + +AuthenticationFactorSms = AuthenticationFactorEnrolledSms diff --git a/src/workos/multi_factor_auth/models/authentication_factor_totp.py b/src/workos/multi_factor_auth/models/authentication_factor_totp.py new file mode 100644 index 00000000..5659d331 --- /dev/null +++ b/src/workos/multi_factor_auth/models/authentication_factor_totp.py @@ -0,0 +1,37 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AuthenticationFactorTotp: + """TOTP-based authentication factor details.""" + + issuer: str + """Your application or company name displayed in the user's authenticator app. Defaults to your WorkOS team name.""" + user: str + """The user's account name displayed in their authenticator app. Defaults to the user's email.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuthenticationFactorTotp": + """Deserialize from a dictionary.""" + try: + return cls( + issuer=data["issuer"], + user=data["user"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuthenticationFactorTotp: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["issuer"] = self.issuer + result["user"] = self.user + return result diff --git a/src/workos/multi_factor_auth/models/authentication_factors_create_request.py b/src/workos/multi_factor_auth/models/authentication_factors_create_request.py new file mode 100644 index 00000000..1df2d977 --- /dev/null +++ b/src/workos/multi_factor_auth/models/authentication_factors_create_request.py @@ -0,0 +1,54 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException +from workos.common.models import AuthenticationFactorsCreateRequestType + + +@dataclass(slots=True) +class AuthenticationFactorsCreateRequest: + """Authentication Factors Create Request model.""" + + type: "AuthenticationFactorsCreateRequestType" + """The type of factor to enroll.""" + phone_number: Optional[str] = None + """Required when type is 'sms'.""" + totp_issuer: Optional[str] = None + """Required when type is 'totp'.""" + totp_user: Optional[str] = None + """Required when type is 'totp'.""" + user_id: Optional[str] = None + """The ID of the user to associate the factor with.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuthenticationFactorsCreateRequest": + """Deserialize from a dictionary.""" + try: + return cls( + type=AuthenticationFactorsCreateRequestType(data["type"]), + phone_number=data.get("phone_number"), + totp_issuer=data.get("totp_issuer"), + totp_user=data.get("totp_user"), + user_id=data.get("user_id"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuthenticationFactorsCreateRequest: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["type"] = self.type + if self.phone_number is not None: + result["phone_number"] = self.phone_number + if self.totp_issuer is not None: + result["totp_issuer"] = self.totp_issuer + if self.totp_user is not None: + result["totp_user"] = self.totp_user + if self.user_id is not None: + result["user_id"] = self.user_id + return result diff --git a/src/workos/multi_factor_auth/models/challenge_authentication_factor.py b/src/workos/multi_factor_auth/models/challenge_authentication_factor.py new file mode 100644 index 00000000..a75d6b31 --- /dev/null +++ b/src/workos/multi_factor_auth/models/challenge_authentication_factor.py @@ -0,0 +1,34 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class ChallengeAuthenticationFactor: + """Challenge Authentication Factor model.""" + + sms_template: Optional[str] = None + """A custom template for the SMS message. Use the {{code}} placeholder to include the verification code.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ChallengeAuthenticationFactor": + """Deserialize from a dictionary.""" + try: + return cls( + sms_template=data.get("sms_template"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing ChallengeAuthenticationFactor: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + if self.sms_template is not None: + result["sms_template"] = self.sms_template + return result diff --git a/src/workos/organization_domains/__init__.py b/src/workos/organization_domains/__init__.py new file mode 100644 index 00000000..8aede658 --- /dev/null +++ b/src/workos/organization_domains/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import OrganizationDomains, AsyncOrganizationDomains +from .models import * diff --git a/src/workos/organization_domains/_resource.py b/src/workos/organization_domains/_resource.py new file mode 100644 index 00000000..09ca3c48 --- /dev/null +++ b/src/workos/organization_domains/_resource.py @@ -0,0 +1,287 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Optional + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import OrganizationDomain, OrganizationDomainStandAlone +from .._types import RequestOptions + + +class OrganizationDomains: + """Organization Domains API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def create_organization_domain( + self, + *, + domain: str, + organization_id: str, + request_options: Optional[RequestOptions] = None, + ) -> OrganizationDomain: + """Create an Organization Domain + + Creates a new Organization Domain. + + Args: + domain: The domain to add to the organization. + organization_id: The ID of the organization to add the domain to. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + OrganizationDomain + + Raises: + ConflictException: If a conflict occurs (409). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "domain": domain, + "organization_id": organization_id, + } + return self._client.request( + method="post", + path="organization_domains", + body=body, + model=OrganizationDomain, + request_options=request_options, + ) + + create = create_organization_domain + + def get_organization_domain( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> OrganizationDomainStandAlone: + """Get an Organization Domain + + Get the details of an existing organization domain. + + Args: + id: Unique identifier of the organization domain. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + OrganizationDomainStandAlone + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"organization_domains/{id}", + model=OrganizationDomainStandAlone, + request_options=request_options, + ) + + get = get_organization_domain + + def delete_organization_domain( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete an Organization Domain + + Permanently deletes an organization domain. It cannot be undone. + + Args: + id: Unique identifier of the organization domain. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + self._client.request( + method="delete", + path=f"organization_domains/{id}", + request_options=request_options, + ) + + delete = delete_organization_domain + + def verify_organization_domain( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> OrganizationDomainStandAlone: + """Verify an Organization Domain + + Initiates verification process for an Organization Domain. + + Args: + id: Unique identifier of the organization domain. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + OrganizationDomainStandAlone + + Raises: + BadRequestException: If the request is malformed (400). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="post", + path=f"organization_domains/{id}/verify", + model=OrganizationDomainStandAlone, + request_options=request_options, + ) + + verify = verify_organization_domain + + +class AsyncOrganizationDomains: + """Organization Domains API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def create_organization_domain( + self, + *, + domain: str, + organization_id: str, + request_options: Optional[RequestOptions] = None, + ) -> OrganizationDomain: + """Create an Organization Domain + + Creates a new Organization Domain. + + Args: + domain: The domain to add to the organization. + organization_id: The ID of the organization to add the domain to. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + OrganizationDomain + + Raises: + ConflictException: If a conflict occurs (409). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "domain": domain, + "organization_id": organization_id, + } + return await self._client.request( + method="post", + path="organization_domains", + body=body, + model=OrganizationDomain, + request_options=request_options, + ) + + create = create_organization_domain + + async def get_organization_domain( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> OrganizationDomainStandAlone: + """Get an Organization Domain + + Get the details of an existing organization domain. + + Args: + id: Unique identifier of the organization domain. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + OrganizationDomainStandAlone + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"organization_domains/{id}", + model=OrganizationDomainStandAlone, + request_options=request_options, + ) + + get = get_organization_domain + + async def delete_organization_domain( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete an Organization Domain + + Permanently deletes an organization domain. It cannot be undone. + + Args: + id: Unique identifier of the organization domain. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + await self._client.request( + method="delete", + path=f"organization_domains/{id}", + request_options=request_options, + ) + + delete = delete_organization_domain + + async def verify_organization_domain( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> OrganizationDomainStandAlone: + """Verify an Organization Domain + + Initiates verification process for an Organization Domain. + + Args: + id: Unique identifier of the organization domain. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + OrganizationDomainStandAlone + + Raises: + BadRequestException: If the request is malformed (400). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="post", + path=f"organization_domains/{id}/verify", + model=OrganizationDomainStandAlone, + request_options=request_options, + ) + + verify = verify_organization_domain diff --git a/src/workos/organization_domains/models/__init__.py b/src/workos/organization_domains/models/__init__.py new file mode 100644 index 00000000..5b4f6869 --- /dev/null +++ b/src/workos/organization_domains/models/__init__.py @@ -0,0 +1,9 @@ +# This file is auto-generated by oagen. Do not edit. + +from .create_organization_domain import ( + CreateOrganizationDomain as CreateOrganizationDomain, +) +from .organization_domain import OrganizationDomain as OrganizationDomain +from .organization_domain_stand_alone import ( + OrganizationDomainStandAlone as OrganizationDomainStandAlone, +) diff --git a/src/workos/organization_domains/models/create_organization_domain.py b/src/workos/organization_domains/models/create_organization_domain.py new file mode 100644 index 00000000..a1200f46 --- /dev/null +++ b/src/workos/organization_domains/models/create_organization_domain.py @@ -0,0 +1,37 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class CreateOrganizationDomain: + """Create Organization Domain model.""" + + domain: str + """The domain to add to the organization.""" + organization_id: str + """The ID of the organization to add the domain to.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CreateOrganizationDomain": + """Deserialize from a dictionary.""" + try: + return cls( + domain=data["domain"], + organization_id=data["organization_id"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing CreateOrganizationDomain: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["domain"] = self.domain + result["organization_id"] = self.organization_id + return result diff --git a/src/workos/organization_domains/models/organization_domain.py b/src/workos/organization_domains/models/organization_domain.py new file mode 100644 index 00000000..426f783d --- /dev/null +++ b/src/workos/organization_domains/models/organization_domain.py @@ -0,0 +1,88 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException +from workos.common.models import OrganizationDomainState +from workos.common.models import OrganizationDomainVerificationStrategy + + +@dataclass(slots=True) +class OrganizationDomain: + """Organization Domain model.""" + + object: Literal["organization_domain"] + """Distinguishes the organization domain object.""" + id: str + """Unique identifier of the organization domain.""" + organization_id: str + """ID of the parent Organization.""" + domain: str + """Domain for the organization domain.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + state: Optional["OrganizationDomainState"] = None + """Verification state of the domain.""" + verification_prefix: Optional[str] = None + """The prefix used in DNS verification.""" + verification_token: Optional[str] = None + """Validation token to be used in DNS TXT record.""" + verification_strategy: Optional["OrganizationDomainVerificationStrategy"] = None + """Strategy used to verify the domain.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "OrganizationDomain": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + organization_id=data["organization_id"], + domain=data["domain"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + state=OrganizationDomainState(_v) + if (_v := data.get("state")) is not None + else None, + verification_prefix=data.get("verification_prefix"), + verification_token=data.get("verification_token"), + verification_strategy=OrganizationDomainVerificationStrategy(_v) + if (_v := data.get("verification_strategy")) is not None + else None, + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing OrganizationDomain: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["organization_id"] = self.organization_id + result["domain"] = self.domain + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + if self.state is not None: + result["state"] = self.state + if self.verification_prefix is not None: + result["verification_prefix"] = self.verification_prefix + if self.verification_token is not None: + result["verification_token"] = self.verification_token + if self.verification_strategy is not None: + result["verification_strategy"] = self.verification_strategy + return result diff --git a/src/workos/organization_domains/models/organization_domain_stand_alone.py b/src/workos/organization_domains/models/organization_domain_stand_alone.py new file mode 100644 index 00000000..0fcbbfee --- /dev/null +++ b/src/workos/organization_domains/models/organization_domain_stand_alone.py @@ -0,0 +1,92 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException +from workos.common.models import OrganizationDomainStandAloneState +from workos.common.models import OrganizationDomainStandAloneVerificationStrategy + + +@dataclass(slots=True) +class OrganizationDomainStandAlone: + """Organization Domain Stand Alone model.""" + + object: Literal["organization_domain"] + """Distinguishes the organization domain object.""" + id: str + """Unique identifier of the organization domain.""" + organization_id: str + """ID of the parent Organization.""" + domain: str + """Domain for the organization domain.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + state: Optional["OrganizationDomainStandAloneState"] = None + """Verification state of the domain.""" + verification_prefix: Optional[str] = None + """The prefix used in DNS verification.""" + verification_token: Optional[str] = None + """Validation token to be used in DNS TXT record.""" + verification_strategy: Optional[ + "OrganizationDomainStandAloneVerificationStrategy" + ] = None + """Strategy used to verify the domain.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "OrganizationDomainStandAlone": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + organization_id=data["organization_id"], + domain=data["domain"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + state=OrganizationDomainStandAloneState(_v) + if (_v := data.get("state")) is not None + else None, + verification_prefix=data.get("verification_prefix"), + verification_token=data.get("verification_token"), + verification_strategy=OrganizationDomainStandAloneVerificationStrategy( + _v + ) + if (_v := data.get("verification_strategy")) is not None + else None, + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing OrganizationDomainStandAlone: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["organization_id"] = self.organization_id + result["domain"] = self.domain + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + if self.state is not None: + result["state"] = self.state + if self.verification_prefix is not None: + result["verification_prefix"] = self.verification_prefix + if self.verification_token is not None: + result["verification_token"] = self.verification_token + if self.verification_strategy is not None: + result["verification_strategy"] = self.verification_strategy + return result diff --git a/src/workos/organizations/__init__.py b/src/workos/organizations/__init__.py new file mode 100644 index 00000000..894fd109 --- /dev/null +++ b/src/workos/organizations/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import Organizations, AsyncOrganizations +from .models import * diff --git a/src/workos/organizations/_resource.py b/src/workos/organizations/_resource.py new file mode 100644 index 00000000..1f9e3bc7 --- /dev/null +++ b/src/workos/organizations/_resource.py @@ -0,0 +1,750 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import ( + AuditLogConfiguration, + AuditLogsRetentionJson, + Organization, + OrganizationDomainData, +) +from .models import OrganizationsOrder +from .._pagination import AsyncPage, SyncPage +from .._types import RequestOptions + + +class Organizations: + """Organizations API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[OrganizationsOrder] = None, + domains: Optional[List[str]] = None, + search: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[Organization]: + """List Organizations + + Get a list of all of your existing organizations matching the criteria specified. + + Args: + domains: The domains of an Organization. Any Organization with a matching domain will be returned. + search: Searchable text for an Organization. Matches against the organization name. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[Organization] + + Raises: + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "domains": domains, + "search": search, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path="organizations", + model=Organization, + params=params, + request_options=request_options, + ) + + def create( + self, + *, + name: str, + allow_profiles_outside_organization: Optional[bool] = None, + domains: Optional[List[str]] = None, + domain_data: Optional[List[OrganizationDomainData]] = None, + metadata: Optional[Dict[str, str]] = None, + external_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> Organization: + """Create an Organization + + Creates a new organization in the current environment. + + Args: + name: The name of the organization. + allow_profiles_outside_organization: Whether the organization allows profiles from outside the organization to sign in. + domains: The domains associated with the organization. Deprecated in favor of `domain_data`. + domain_data: The domains associated with the organization, including verification state. + metadata: Object containing [metadata](https://workos.com/docs/authkit/metadata) key/value pairs associated with the Organization. + external_id: An external identifier for the Organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Organization + + Raises: + BadRequestException: If the request is malformed (400). + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "name": name, + "allow_profiles_outside_organization": allow_profiles_outside_organization, + "domains": domains, + "domain_data": [item.to_dict() for item in domain_data] + if domain_data is not None + else None, + "metadata": metadata, + "external_id": external_id, + }.items() + if v is not None + } + return self._client.request( + method="post", + path="organizations", + body=body, + model=Organization, + request_options=request_options, + ) + + def get_by_external_id( + self, + external_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Organization: + """Get an Organization by External ID + + Get the details of an existing organization by an [external identifier](https://workos.com/docs/authkit/metadata/external-identifiers). + + Args: + external_id: The external ID of the Organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Organization + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"organizations/external_id/{external_id}", + model=Organization, + request_options=request_options, + ) + + def get( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Organization: + """Get an Organization + + Get the details of an existing organization. + + Args: + id: Unique identifier of the Organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Organization + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"organizations/{id}", + model=Organization, + request_options=request_options, + ) + + find = get + + def update( + self, + id: str, + *, + name: Optional[str] = None, + allow_profiles_outside_organization: Optional[bool] = None, + domains: Optional[List[str]] = None, + domain_data: Optional[List[OrganizationDomainData]] = None, + stripe_customer_id: Optional[str] = None, + metadata: Optional[Dict[str, str]] = None, + external_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> Organization: + """Update an Organization + + Updates an organization in the current environment. + + Args: + id: Unique identifier of the Organization. + name: The name of the organization. + allow_profiles_outside_organization: Whether the organization allows profiles from outside the organization to sign in. + domains: The domains associated with the organization. Deprecated in favor of `domain_data`. + domain_data: The domains associated with the organization, including verification state. + stripe_customer_id: The Stripe customer ID associated with the organization. + metadata: Object containing [metadata](https://workos.com/docs/authkit/metadata) key/value pairs associated with the Organization. + external_id: An external identifier for the Organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Organization + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "name": name, + "allow_profiles_outside_organization": allow_profiles_outside_organization, + "domains": domains, + "domain_data": [item.to_dict() for item in domain_data] + if domain_data is not None + else None, + "stripe_customer_id": stripe_customer_id, + "metadata": metadata, + "external_id": external_id, + }.items() + if v is not None + } + return self._client.request( + method="put", + path=f"organizations/{id}", + body=body, + model=Organization, + request_options=request_options, + ) + + def delete( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete an Organization + + Permanently deletes an organization in the current environment. It cannot be undone. + + Args: + id: Unique identifier of the Organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + AuthorizationException: If the request is forbidden (403). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + self._client.request( + method="delete", + path=f"organizations/{id}", + request_options=request_options, + ) + + def get_audit_log_configuration( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> AuditLogConfiguration: + """Get Audit Log Configuration + + Get the unified view of audit log trail and stream configuration for an organization. + + Args: + id: Unique identifier of the Organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuditLogConfiguration + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"organizations/{id}/audit_log_configuration", + model=AuditLogConfiguration, + request_options=request_options, + ) + + def audit_logs_retention( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> AuditLogsRetentionJson: + """Get Retention + + Get the configured event retention period for the given Organization. + + Args: + id: Unique identifier of the Organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuditLogsRetentionJson + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"organizations/{id}/audit_logs_retention", + model=AuditLogsRetentionJson, + request_options=request_options, + ) + + def update_audit_logs_retention( + self, + id: str, + *, + retention_period_in_days: int, + request_options: Optional[RequestOptions] = None, + ) -> AuditLogsRetentionJson: + """Set Retention + + Set the event retention period for the given Organization. + + Args: + id: Unique identifier of the Organization. + retention_period_in_days: The number of days Audit Log events will be retained. Valid values are `30` and `365`. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuditLogsRetentionJson + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "retention_period_in_days": retention_period_in_days, + } + return self._client.request( + method="put", + path=f"organizations/{id}/audit_logs_retention", + body=body, + model=AuditLogsRetentionJson, + request_options=request_options, + ) + + +class AsyncOrganizations: + """Organizations API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[OrganizationsOrder] = None, + domains: Optional[List[str]] = None, + search: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[Organization]: + """List Organizations + + Get a list of all of your existing organizations matching the criteria specified. + + Args: + domains: The domains of an Organization. Any Organization with a matching domain will be returned. + search: Searchable text for an Organization. Matches against the organization name. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[Organization] + + Raises: + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "domains": domains, + "search": search, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path="organizations", + model=Organization, + params=params, + request_options=request_options, + ) + + async def create( + self, + *, + name: str, + allow_profiles_outside_organization: Optional[bool] = None, + domains: Optional[List[str]] = None, + domain_data: Optional[List[OrganizationDomainData]] = None, + metadata: Optional[Dict[str, str]] = None, + external_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> Organization: + """Create an Organization + + Creates a new organization in the current environment. + + Args: + name: The name of the organization. + allow_profiles_outside_organization: Whether the organization allows profiles from outside the organization to sign in. + domains: The domains associated with the organization. Deprecated in favor of `domain_data`. + domain_data: The domains associated with the organization, including verification state. + metadata: Object containing [metadata](https://workos.com/docs/authkit/metadata) key/value pairs associated with the Organization. + external_id: An external identifier for the Organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Organization + + Raises: + BadRequestException: If the request is malformed (400). + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "name": name, + "allow_profiles_outside_organization": allow_profiles_outside_organization, + "domains": domains, + "domain_data": [item.to_dict() for item in domain_data] + if domain_data is not None + else None, + "metadata": metadata, + "external_id": external_id, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path="organizations", + body=body, + model=Organization, + request_options=request_options, + ) + + async def get_by_external_id( + self, + external_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Organization: + """Get an Organization by External ID + + Get the details of an existing organization by an [external identifier](https://workos.com/docs/authkit/metadata/external-identifiers). + + Args: + external_id: The external ID of the Organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Organization + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"organizations/external_id/{external_id}", + model=Organization, + request_options=request_options, + ) + + async def get( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Organization: + """Get an Organization + + Get the details of an existing organization. + + Args: + id: Unique identifier of the Organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Organization + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"organizations/{id}", + model=Organization, + request_options=request_options, + ) + + find = get + + async def update( + self, + id: str, + *, + name: Optional[str] = None, + allow_profiles_outside_organization: Optional[bool] = None, + domains: Optional[List[str]] = None, + domain_data: Optional[List[OrganizationDomainData]] = None, + stripe_customer_id: Optional[str] = None, + metadata: Optional[Dict[str, str]] = None, + external_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> Organization: + """Update an Organization + + Updates an organization in the current environment. + + Args: + id: Unique identifier of the Organization. + name: The name of the organization. + allow_profiles_outside_organization: Whether the organization allows profiles from outside the organization to sign in. + domains: The domains associated with the organization. Deprecated in favor of `domain_data`. + domain_data: The domains associated with the organization, including verification state. + stripe_customer_id: The Stripe customer ID associated with the organization. + metadata: Object containing [metadata](https://workos.com/docs/authkit/metadata) key/value pairs associated with the Organization. + external_id: An external identifier for the Organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Organization + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "name": name, + "allow_profiles_outside_organization": allow_profiles_outside_organization, + "domains": domains, + "domain_data": [item.to_dict() for item in domain_data] + if domain_data is not None + else None, + "stripe_customer_id": stripe_customer_id, + "metadata": metadata, + "external_id": external_id, + }.items() + if v is not None + } + return await self._client.request( + method="put", + path=f"organizations/{id}", + body=body, + model=Organization, + request_options=request_options, + ) + + async def delete( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete an Organization + + Permanently deletes an organization in the current environment. It cannot be undone. + + Args: + id: Unique identifier of the Organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + AuthorizationException: If the request is forbidden (403). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + await self._client.request( + method="delete", + path=f"organizations/{id}", + request_options=request_options, + ) + + async def get_audit_log_configuration( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> AuditLogConfiguration: + """Get Audit Log Configuration + + Get the unified view of audit log trail and stream configuration for an organization. + + Args: + id: Unique identifier of the Organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuditLogConfiguration + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"organizations/{id}/audit_log_configuration", + model=AuditLogConfiguration, + request_options=request_options, + ) + + async def audit_logs_retention( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> AuditLogsRetentionJson: + """Get Retention + + Get the configured event retention period for the given Organization. + + Args: + id: Unique identifier of the Organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuditLogsRetentionJson + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"organizations/{id}/audit_logs_retention", + model=AuditLogsRetentionJson, + request_options=request_options, + ) + + async def update_audit_logs_retention( + self, + id: str, + *, + retention_period_in_days: int, + request_options: Optional[RequestOptions] = None, + ) -> AuditLogsRetentionJson: + """Set Retention + + Set the event retention period for the given Organization. + + Args: + id: Unique identifier of the Organization. + retention_period_in_days: The number of days Audit Log events will be retained. Valid values are `30` and `365`. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuditLogsRetentionJson + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "retention_period_in_days": retention_period_in_days, + } + return await self._client.request( + method="put", + path=f"organizations/{id}/audit_logs_retention", + body=body, + model=AuditLogsRetentionJson, + request_options=request_options, + ) diff --git a/src/workos/organizations/api_keys/__init__.py b/src/workos/organizations/api_keys/__init__.py new file mode 100644 index 00000000..ec3158be --- /dev/null +++ b/src/workos/organizations/api_keys/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import OrganizationsApiKeys, AsyncOrganizationsApiKeys +from .models import * diff --git a/src/workos/organizations/api_keys/_resource.py b/src/workos/organizations/api_keys/_resource.py new file mode 100644 index 00000000..59eb2da2 --- /dev/null +++ b/src/workos/organizations/api_keys/_resource.py @@ -0,0 +1,214 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +if TYPE_CHECKING: + from ..._client import AsyncWorkOSClient, WorkOSClient + +from .models import ApiKeyWithValue +from workos.api_keys.models import ApiKey +from .models import OrganizationsApiKeysOrder +from ..._pagination import AsyncPage, SyncPage +from ..._types import RequestOptions + + +class OrganizationsApiKeys: + """Organizations Api Keys API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def list( + self, + organization_id: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[OrganizationsApiKeysOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[ApiKey]: + """List API keys for an organization + + Get a list of all API keys for an organization. + + Args: + organization_id: Unique identifier of the Organization. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[ApiKey] + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path=f"organizations/{organization_id}/api_keys", + model=ApiKey, + params=params, + request_options=request_options, + ) + + def create( + self, + organization_id: str, + *, + name: str, + permissions: Optional[List[str]] = None, + request_options: Optional[RequestOptions] = None, + ) -> ApiKeyWithValue: + """Create an API key for an organization + + Create a new API key for an organization. + + Args: + organization_id: Unique identifier of the Organization. + name: The name for the API key. + permissions: The permission slugs to assign to the API key. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + ApiKeyWithValue + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "name": name, + "permissions": permissions, + }.items() + if v is not None + } + return self._client.request( + method="post", + path=f"organizations/{organization_id}/api_keys", + body=body, + model=ApiKeyWithValue, + request_options=request_options, + ) + + +class AsyncOrganizationsApiKeys: + """Organizations Api Keys API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def list( + self, + organization_id: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[OrganizationsApiKeysOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[ApiKey]: + """List API keys for an organization + + Get a list of all API keys for an organization. + + Args: + organization_id: Unique identifier of the Organization. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[ApiKey] + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path=f"organizations/{organization_id}/api_keys", + model=ApiKey, + params=params, + request_options=request_options, + ) + + async def create( + self, + organization_id: str, + *, + name: str, + permissions: Optional[List[str]] = None, + request_options: Optional[RequestOptions] = None, + ) -> ApiKeyWithValue: + """Create an API key for an organization + + Create a new API key for an organization. + + Args: + organization_id: Unique identifier of the Organization. + name: The name for the API key. + permissions: The permission slugs to assign to the API key. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + ApiKeyWithValue + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "name": name, + "permissions": permissions, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path=f"organizations/{organization_id}/api_keys", + body=body, + model=ApiKeyWithValue, + request_options=request_options, + ) diff --git a/src/workos/organizations/api_keys/models/__init__.py b/src/workos/organizations/api_keys/models/__init__.py new file mode 100644 index 00000000..6bafeae3 --- /dev/null +++ b/src/workos/organizations/api_keys/models/__init__.py @@ -0,0 +1,10 @@ +# This file is auto-generated by oagen. Do not edit. + +from .api_key_with_value import ApiKeyWithValue as ApiKeyWithValue +from .api_key_with_value_owner import ApiKeyWithValueOwner as ApiKeyWithValueOwner +from .create_organization_api_key import ( + CreateOrganizationApiKey as CreateOrganizationApiKey, +) +from .organizations_api_keys_order import ( + OrganizationsApiKeysOrder as OrganizationsApiKeysOrder, +) diff --git a/src/workos/organizations/api_keys/models/api_key_with_value.py b/src/workos/organizations/api_keys/models/api_key_with_value.py new file mode 100644 index 00000000..5ba9a613 --- /dev/null +++ b/src/workos/organizations/api_keys/models/api_key_with_value.py @@ -0,0 +1,86 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import cast +from typing import Any, Dict, List, Literal, Optional +from workos._errors import BaseRequestException + +from .api_key_with_value_owner import ApiKeyWithValueOwner + + +@dataclass(slots=True) +class ApiKeyWithValue: + """Api Key With Value model.""" + + object: Literal["api_key"] + """Distinguishes the API Key object.""" + id: str + """Unique identifier of the API Key.""" + owner: "ApiKeyWithValueOwner" + """The entity that owns the API Key.""" + name: str + """A descriptive name for the API Key.""" + obfuscated_value: str + """An obfuscated representation of the API Key value.""" + last_used_at: Optional[str] + """Timestamp of when the API Key was last used.""" + permissions: List[str] + """The permission slugs assigned to the API Key.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + value: str + """The full API Key value. Only returned once at creation time.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ApiKeyWithValue": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + owner=ApiKeyWithValueOwner.from_dict( + cast(Dict[str, Any], data["owner"]) + ), + name=data["name"], + obfuscated_value=data["obfuscated_value"], + last_used_at=data["last_used_at"], + permissions=data["permissions"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + value=data["value"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing ApiKeyWithValue: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["owner"] = self.owner.to_dict() + result["name"] = self.name + result["obfuscated_value"] = self.obfuscated_value + if self.last_used_at is not None: + result["last_used_at"] = self.last_used_at + else: + result["last_used_at"] = None + result["permissions"] = self.permissions + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["value"] = self.value + return result diff --git a/src/workos/organizations/api_keys/models/api_key_with_value_owner.py b/src/workos/organizations/api_keys/models/api_key_with_value_owner.py new file mode 100644 index 00000000..1e55ac7d --- /dev/null +++ b/src/workos/organizations/api_keys/models/api_key_with_value_owner.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.api_keys.models import ApiKeyOwner + +ApiKeyWithValueOwner = ApiKeyOwner diff --git a/src/workos/organizations/api_keys/models/create_organization_api_key.py b/src/workos/organizations/api_keys/models/create_organization_api_key.py new file mode 100644 index 00000000..4eadb761 --- /dev/null +++ b/src/workos/organizations/api_keys/models/create_organization_api_key.py @@ -0,0 +1,38 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, List, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class CreateOrganizationApiKey: + """Create Organization Api Key model.""" + + name: str + """The name for the API key.""" + permissions: Optional[List[str]] = None + """The permission slugs to assign to the API key.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CreateOrganizationApiKey": + """Deserialize from a dictionary.""" + try: + return cls( + name=data["name"], + permissions=data.get("permissions"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing CreateOrganizationApiKey: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["name"] = self.name + if self.permissions is not None: + result["permissions"] = self.permissions + return result diff --git a/src/workos/organizations/api_keys/models/organizations_api_keys_order.py b/src/workos/organizations/api_keys/models/organizations_api_keys_order.py new file mode 100644 index 00000000..e77b7a9e --- /dev/null +++ b/src/workos/organizations/api_keys/models/organizations_api_keys_order.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import ApplicationsOrder + +OrganizationsApiKeysOrder = ApplicationsOrder +__all__ = ["OrganizationsApiKeysOrder"] diff --git a/src/workos/organizations/feature_flags/__init__.py b/src/workos/organizations/feature_flags/__init__.py new file mode 100644 index 00000000..a01f74af --- /dev/null +++ b/src/workos/organizations/feature_flags/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import OrganizationsFeatureFlags, AsyncOrganizationsFeatureFlags +from .models import * diff --git a/src/workos/organizations/feature_flags/_resource.py b/src/workos/organizations/feature_flags/_resource.py new file mode 100644 index 00000000..343c1a53 --- /dev/null +++ b/src/workos/organizations/feature_flags/_resource.py @@ -0,0 +1,125 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from ..._client import AsyncWorkOSClient, WorkOSClient + +from workos.feature_flags.models import Flag +from .models import OrganizationsFeatureFlagsOrder +from ..._pagination import AsyncPage, SyncPage +from ..._types import RequestOptions + + +class OrganizationsFeatureFlags: + """Organizations Feature Flags API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def list( + self, + organization_id: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[OrganizationsFeatureFlagsOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[Flag]: + """List enabled feature flags for an organization + + Get a list of all enabled feature flags for an organization. + + Args: + organization_id: Unique identifier of the Organization. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[Flag] + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path=f"organizations/{organization_id}/feature-flags", + model=Flag, + params=params, + request_options=request_options, + ) + + +class AsyncOrganizationsFeatureFlags: + """Organizations Feature Flags API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def list( + self, + organization_id: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[OrganizationsFeatureFlagsOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[Flag]: + """List enabled feature flags for an organization + + Get a list of all enabled feature flags for an organization. + + Args: + organization_id: Unique identifier of the Organization. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[Flag] + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path=f"organizations/{organization_id}/feature-flags", + model=Flag, + params=params, + request_options=request_options, + ) diff --git a/src/workos/organizations/feature_flags/models/__init__.py b/src/workos/organizations/feature_flags/models/__init__.py new file mode 100644 index 00000000..3b12b4eb --- /dev/null +++ b/src/workos/organizations/feature_flags/models/__init__.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from .organizations_feature_flags_order import ( + OrganizationsFeatureFlagsOrder as OrganizationsFeatureFlagsOrder, +) diff --git a/src/workos/organizations/feature_flags/models/organizations_feature_flags_order.py b/src/workos/organizations/feature_flags/models/organizations_feature_flags_order.py new file mode 100644 index 00000000..f44d1ef0 --- /dev/null +++ b/src/workos/organizations/feature_flags/models/organizations_feature_flags_order.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import ApplicationsOrder + +OrganizationsFeatureFlagsOrder = ApplicationsOrder +__all__ = ["OrganizationsFeatureFlagsOrder"] diff --git a/src/workos/organizations/models/__init__.py b/src/workos/organizations/models/__init__.py new file mode 100644 index 00000000..d4d98494 --- /dev/null +++ b/src/workos/organizations/models/__init__.py @@ -0,0 +1,15 @@ +# This file is auto-generated by oagen. Do not edit. + +from .audit_log_configuration import AuditLogConfiguration as AuditLogConfiguration +from .audit_log_configuration_log_stream import ( + AuditLogConfigurationLogStream as AuditLogConfigurationLogStream, +) +from .audit_logs_retention_json import AuditLogsRetentionJson as AuditLogsRetentionJson +from .organization import Organization as Organization +from .organization_domain_data import OrganizationDomainData as OrganizationDomainData +from .organization_dto import OrganizationDto as OrganizationDto +from .organizations_order import OrganizationsOrder as OrganizationsOrder +from .update_audit_logs_retention import ( + UpdateAuditLogsRetention as UpdateAuditLogsRetention, +) +from .update_organization import UpdateOrganization as UpdateOrganization diff --git a/src/workos/organizations/models/audit_log_configuration.py b/src/workos/organizations/models/audit_log_configuration.py new file mode 100644 index 00000000..3674bf6e --- /dev/null +++ b/src/workos/organizations/models/audit_log_configuration.py @@ -0,0 +1,54 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + +from .audit_log_configuration_log_stream import AuditLogConfigurationLogStream +from workos.common.models import AuditLogConfigurationState + + +@dataclass(slots=True) +class AuditLogConfiguration: + """Audit Log Configuration model.""" + + organization_id: str + """Unique identifier of the Organization.""" + retention_period_in_days: int + """The number of days Audit Log events will be retained before being permanently deleted.""" + state: "AuditLogConfigurationState" + """The current state of the audit log configuration for the organization.""" + log_stream: Optional["AuditLogConfigurationLogStream"] = None + """The Audit Log Stream currently configured for the organization, if any.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuditLogConfiguration": + """Deserialize from a dictionary.""" + try: + return cls( + organization_id=data["organization_id"], + retention_period_in_days=data["retention_period_in_days"], + state=AuditLogConfigurationState(data["state"]), + log_stream=AuditLogConfigurationLogStream.from_dict( + cast(Dict[str, Any], _v) + ) + if (_v := data.get("log_stream")) is not None + else None, + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuditLogConfiguration: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["organization_id"] = self.organization_id + result["retention_period_in_days"] = self.retention_period_in_days + result["state"] = self.state + if self.log_stream is not None: + result["log_stream"] = self.log_stream.to_dict() + return result diff --git a/src/workos/organizations/models/audit_log_configuration_log_stream.py b/src/workos/organizations/models/audit_log_configuration_log_stream.py new file mode 100644 index 00000000..2765ba30 --- /dev/null +++ b/src/workos/organizations/models/audit_log_configuration_log_stream.py @@ -0,0 +1,59 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException +from workos.common.models import AuditLogConfigurationLogStreamState +from workos.common.models import AuditLogConfigurationLogStreamType + + +@dataclass(slots=True) +class AuditLogConfigurationLogStream: + """The Audit Log Stream currently configured for the organization, if any.""" + + id: str + """Unique identifier of the Audit Log Stream.""" + type: "AuditLogConfigurationLogStreamType" + """The type of the Audit Log Stream destination.""" + state: "AuditLogConfigurationLogStreamState" + """The current state of the Audit Log Stream.""" + last_synced_at: Optional[str] + """ISO-8601 timestamp of when the last event was successfully synced, or null if no events have been synced.""" + created_at: datetime + """An ISO 8601 timestamp.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuditLogConfigurationLogStream": + """Deserialize from a dictionary.""" + try: + return cls( + id=data["id"], + type=AuditLogConfigurationLogStreamType(data["type"]), + state=AuditLogConfigurationLogStreamState(data["state"]), + last_synced_at=data["last_synced_at"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuditLogConfigurationLogStream: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["id"] = self.id + result["type"] = self.type + result["state"] = self.state + if self.last_synced_at is not None: + result["last_synced_at"] = self.last_synced_at + else: + result["last_synced_at"] = None + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + return result diff --git a/src/workos/organizations/models/audit_logs_retention_json.py b/src/workos/organizations/models/audit_logs_retention_json.py new file mode 100644 index 00000000..7720076c --- /dev/null +++ b/src/workos/organizations/models/audit_logs_retention_json.py @@ -0,0 +1,36 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AuditLogsRetentionJson: + """Audit Logs Retention Json model.""" + + retention_period_in_days: Optional[int] + """The number of days Audit Log events will be retained before being permanently deleted. Valid values are 30 and 365.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuditLogsRetentionJson": + """Deserialize from a dictionary.""" + try: + return cls( + retention_period_in_days=data["retention_period_in_days"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuditLogsRetentionJson: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + if self.retention_period_in_days is not None: + result["retention_period_in_days"] = self.retention_period_in_days + else: + result["retention_period_in_days"] = None + return result diff --git a/src/workos/organizations/models/organization.py b/src/workos/organizations/models/organization.py new file mode 100644 index 00000000..be8f0493 --- /dev/null +++ b/src/workos/organizations/models/organization.py @@ -0,0 +1,92 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import cast +from typing import Any, Dict, List, Literal, Optional +from workos._errors import BaseRequestException + +from workos.organization_domains.models import OrganizationDomain + + +@dataclass(slots=True) +class Organization: + """Organization model.""" + + object: Literal["organization"] + """Distinguishes the Organization object.""" + id: str + """Unique identifier of the Organization.""" + name: str + """A descriptive name for the Organization. This field does not need to be unique.""" + domains: List["OrganizationDomain"] + """List of Organization Domains.""" + metadata: Dict[str, str] + """Object containing [metadata](https://workos.com/docs/authkit/metadata) key/value pairs associated with the Organization.""" + external_id: Optional[str] + """The external ID of the Organization.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + allow_profiles_outside_organization: bool + """Whether the Organization allows profiles outside of its managed domains.""" + stripe_customer_id: Optional[str] = None + """The Stripe customer ID of the Organization.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Organization": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + name=data["name"], + domains=[ + OrganizationDomain.from_dict(cast(Dict[str, Any], item)) + for item in cast(list[Any], data["domains"]) + ], + metadata=data["metadata"], + external_id=data["external_id"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + allow_profiles_outside_organization=data[ + "allow_profiles_outside_organization" + ], + stripe_customer_id=data.get("stripe_customer_id"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing Organization: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["name"] = self.name + result["domains"] = [item.to_dict() for item in self.domains] + result["metadata"] = self.metadata + if self.external_id is not None: + result["external_id"] = self.external_id + else: + result["external_id"] = None + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["allow_profiles_outside_organization"] = ( + self.allow_profiles_outside_organization + ) + if self.stripe_customer_id is not None: + result["stripe_customer_id"] = self.stripe_customer_id + return result diff --git a/src/workos/organizations/models/organization_domain_data.py b/src/workos/organizations/models/organization_domain_data.py new file mode 100644 index 00000000..daac8858 --- /dev/null +++ b/src/workos/organizations/models/organization_domain_data.py @@ -0,0 +1,38 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException +from workos.common.models import OrganizationDomainDataDtoState + + +@dataclass(slots=True) +class OrganizationDomainData: + """Organization Domain Data model.""" + + domain: str + """The domain value.""" + state: "OrganizationDomainDataDtoState" + """The verification state of the domain.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "OrganizationDomainData": + """Deserialize from a dictionary.""" + try: + return cls( + domain=data["domain"], + state=OrganizationDomainDataDtoState(data["state"]), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing OrganizationDomainData: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["domain"] = self.domain + result["state"] = self.state + return result diff --git a/src/workos/organizations/models/organization_dto.py b/src/workos/organizations/models/organization_dto.py new file mode 100644 index 00000000..2834f4ae --- /dev/null +++ b/src/workos/organizations/models/organization_dto.py @@ -0,0 +1,74 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, List, Optional +from workos._errors import BaseRequestException + +from .organization_domain_data import OrganizationDomainData + + +@dataclass(slots=True) +class OrganizationDto: + """Organization Dto model.""" + + name: str + """The name of the organization.""" + allow_profiles_outside_organization: Optional[bool] = None + """Whether the organization allows profiles from outside the organization to sign in.""" + domains: Optional[List[str]] = None + """The domains associated with the organization. Deprecated in favor of `domain_data`.""" + domain_data: Optional[List["OrganizationDomainData"]] = None + """The domains associated with the organization, including verification state.""" + metadata: Optional[Dict[str, str]] = None + """Object containing [metadata](https://workos.com/docs/authkit/metadata) key/value pairs associated with the Organization.""" + external_id: Optional[str] = None + """An external identifier for the Organization.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "OrganizationDto": + """Deserialize from a dictionary.""" + try: + return cls( + name=data["name"], + allow_profiles_outside_organization=data.get( + "allow_profiles_outside_organization" + ), + domains=data.get("domains"), + domain_data=[ + OrganizationDomainData.from_dict(cast(Dict[str, Any], item)) + for item in cast(list[Any], _v) + ] + if (_v := data.get("domain_data")) is not None + else None, + metadata=data.get("metadata"), + external_id=data.get("external_id"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing OrganizationDto: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["name"] = self.name + if self.allow_profiles_outside_organization is not None: + result["allow_profiles_outside_organization"] = ( + self.allow_profiles_outside_organization + ) + if self.domains is not None: + result["domains"] = self.domains + if self.domain_data is not None: + result["domain_data"] = [item.to_dict() for item in self.domain_data] + if self.metadata is not None: + result["metadata"] = self.metadata + else: + result["metadata"] = None + if self.external_id is not None: + result["external_id"] = self.external_id + else: + result["external_id"] = None + return result diff --git a/src/workos/organizations/models/organizations_order.py b/src/workos/organizations/models/organizations_order.py new file mode 100644 index 00000000..1f36d53b --- /dev/null +++ b/src/workos/organizations/models/organizations_order.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import ApplicationsOrder + +OrganizationsOrder = ApplicationsOrder +__all__ = ["OrganizationsOrder"] diff --git a/src/workos/organizations/models/update_audit_logs_retention.py b/src/workos/organizations/models/update_audit_logs_retention.py new file mode 100644 index 00000000..b9aa1bf8 --- /dev/null +++ b/src/workos/organizations/models/update_audit_logs_retention.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class UpdateAuditLogsRetention: + """Update Audit Logs Retention model.""" + + retention_period_in_days: int + """The number of days Audit Log events will be retained. Valid values are `30` and `365`.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "UpdateAuditLogsRetention": + """Deserialize from a dictionary.""" + try: + return cls( + retention_period_in_days=data["retention_period_in_days"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UpdateAuditLogsRetention: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["retention_period_in_days"] = self.retention_period_in_days + return result diff --git a/src/workos/organizations/models/update_organization.py b/src/workos/organizations/models/update_organization.py new file mode 100644 index 00000000..4470e8a4 --- /dev/null +++ b/src/workos/organizations/models/update_organization.py @@ -0,0 +1,80 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, List, Optional +from workos._errors import BaseRequestException + +from .organization_domain_data import OrganizationDomainData + + +@dataclass(slots=True) +class UpdateOrganization: + """Update Organization model.""" + + name: Optional[str] = None + """The name of the organization.""" + allow_profiles_outside_organization: Optional[bool] = None + """Whether the organization allows profiles from outside the organization to sign in.""" + domains: Optional[List[str]] = None + """The domains associated with the organization. Deprecated in favor of `domain_data`.""" + domain_data: Optional[List["OrganizationDomainData"]] = None + """The domains associated with the organization, including verification state.""" + stripe_customer_id: Optional[str] = None + """The Stripe customer ID associated with the organization.""" + metadata: Optional[Dict[str, str]] = None + """Object containing [metadata](https://workos.com/docs/authkit/metadata) key/value pairs associated with the Organization.""" + external_id: Optional[str] = None + """An external identifier for the Organization.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "UpdateOrganization": + """Deserialize from a dictionary.""" + try: + return cls( + name=data.get("name"), + allow_profiles_outside_organization=data.get( + "allow_profiles_outside_organization" + ), + domains=data.get("domains"), + domain_data=[ + OrganizationDomainData.from_dict(cast(Dict[str, Any], item)) + for item in cast(list[Any], _v) + ] + if (_v := data.get("domain_data")) is not None + else None, + stripe_customer_id=data.get("stripe_customer_id"), + metadata=data.get("metadata"), + external_id=data.get("external_id"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UpdateOrganization: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + if self.name is not None: + result["name"] = self.name + if self.allow_profiles_outside_organization is not None: + result["allow_profiles_outside_organization"] = ( + self.allow_profiles_outside_organization + ) + if self.domains is not None: + result["domains"] = self.domains + if self.domain_data is not None: + result["domain_data"] = [item.to_dict() for item in self.domain_data] + if self.stripe_customer_id is not None: + result["stripe_customer_id"] = self.stripe_customer_id + if self.metadata is not None: + result["metadata"] = self.metadata + else: + result["metadata"] = None + if self.external_id is not None: + result["external_id"] = self.external_id + else: + result["external_id"] = None + return result diff --git a/src/workos/permissions/__init__.py b/src/workos/permissions/__init__.py new file mode 100644 index 00000000..24bea04e --- /dev/null +++ b/src/workos/permissions/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import Permissions, AsyncPermissions +from .models import * diff --git a/src/workos/permissions/_resource.py b/src/workos/permissions/_resource.py new file mode 100644 index 00000000..847fe551 --- /dev/null +++ b/src/workos/permissions/_resource.py @@ -0,0 +1,429 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Optional + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import AuthorizationPermission, Permission +from .models import PermissionsOrder +from .._pagination import AsyncPage, SyncPage +from .._types import RequestOptions + + +class Permissions: + """Permissions API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[PermissionsOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[AuthorizationPermission]: + """List permissions + + Get a list of all permissions in your WorkOS environment. + + Args: + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[AuthorizationPermission] + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path="authorization/permissions", + model=AuthorizationPermission, + params=params, + request_options=request_options, + ) + + def create( + self, + *, + slug: str, + name: str, + description: Optional[str] = None, + resource_type_slug: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> Permission: + """Create a permission + + Create a new permission in your WorkOS environment. The permission can then be assigned to environment roles and organization roles. + + Args: + slug: A unique key to reference the permission. Must be lowercase and contain only letters, numbers, hyphens, underscores, colons, periods, and asterisks. + name: A descriptive name for the Permission. + description: An optional description of the Permission. + resource_type_slug: The slug of the resource type this permission is scoped to. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Permission + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "slug": slug, + "name": name, + "description": description, + "resource_type_slug": resource_type_slug, + }.items() + if v is not None + } + return self._client.request( + method="post", + path="authorization/permissions", + body=body, + model=Permission, + request_options=request_options, + ) + + def get( + self, + slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> AuthorizationPermission: + """Get a permission + + Retrieve a permission by its unique slug. + + Args: + slug: A unique key to reference the permission. Must be lowercase and contain only letters, numbers, hyphens, underscores, colons, periods, and asterisks. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthorizationPermission + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"authorization/permissions/{slug}", + model=AuthorizationPermission, + request_options=request_options, + ) + + find = get + + def update( + self, + slug: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AuthorizationPermission: + """Update a permission + + Update an existing permission. Only the fields provided in the request body will be updated. + + Args: + slug: A unique key to reference the permission. Must be lowercase and contain only letters, numbers, hyphens, underscores, colons, periods, and asterisks. + name: A descriptive name for the Permission. + description: An optional description of the Permission. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthorizationPermission + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "name": name, + "description": description, + }.items() + if v is not None + } + return self._client.request( + method="patch", + path=f"authorization/permissions/{slug}", + body=body, + model=AuthorizationPermission, + request_options=request_options, + ) + + def delete( + self, + slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a permission + + Delete an existing permission. System permissions cannot be deleted. + + Args: + slug: A unique key to reference the permission. Must be lowercase and contain only letters, numbers, hyphens, underscores, colons, periods, and asterisks. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + self._client.request( + method="delete", + path=f"authorization/permissions/{slug}", + request_options=request_options, + ) + + +class AsyncPermissions: + """Permissions API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[PermissionsOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[AuthorizationPermission]: + """List permissions + + Get a list of all permissions in your WorkOS environment. + + Args: + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[AuthorizationPermission] + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path="authorization/permissions", + model=AuthorizationPermission, + params=params, + request_options=request_options, + ) + + async def create( + self, + *, + slug: str, + name: str, + description: Optional[str] = None, + resource_type_slug: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> Permission: + """Create a permission + + Create a new permission in your WorkOS environment. The permission can then be assigned to environment roles and organization roles. + + Args: + slug: A unique key to reference the permission. Must be lowercase and contain only letters, numbers, hyphens, underscores, colons, periods, and asterisks. + name: A descriptive name for the Permission. + description: An optional description of the Permission. + resource_type_slug: The slug of the resource type this permission is scoped to. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Permission + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "slug": slug, + "name": name, + "description": description, + "resource_type_slug": resource_type_slug, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path="authorization/permissions", + body=body, + model=Permission, + request_options=request_options, + ) + + async def get( + self, + slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> AuthorizationPermission: + """Get a permission + + Retrieve a permission by its unique slug. + + Args: + slug: A unique key to reference the permission. Must be lowercase and contain only letters, numbers, hyphens, underscores, colons, periods, and asterisks. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthorizationPermission + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"authorization/permissions/{slug}", + model=AuthorizationPermission, + request_options=request_options, + ) + + find = get + + async def update( + self, + slug: str, + *, + name: Optional[str] = None, + description: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AuthorizationPermission: + """Update a permission + + Update an existing permission. Only the fields provided in the request body will be updated. + + Args: + slug: A unique key to reference the permission. Must be lowercase and contain only letters, numbers, hyphens, underscores, colons, periods, and asterisks. + name: A descriptive name for the Permission. + description: An optional description of the Permission. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthorizationPermission + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "name": name, + "description": description, + }.items() + if v is not None + } + return await self._client.request( + method="patch", + path=f"authorization/permissions/{slug}", + body=body, + model=AuthorizationPermission, + request_options=request_options, + ) + + async def delete( + self, + slug: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a permission + + Delete an existing permission. System permissions cannot be deleted. + + Args: + slug: A unique key to reference the permission. Must be lowercase and contain only letters, numbers, hyphens, underscores, colons, periods, and asterisks. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + await self._client.request( + method="delete", + path=f"authorization/permissions/{slug}", + request_options=request_options, + ) diff --git a/src/workos/permissions/models/__init__.py b/src/workos/permissions/models/__init__.py new file mode 100644 index 00000000..453a66b4 --- /dev/null +++ b/src/workos/permissions/models/__init__.py @@ -0,0 +1,11 @@ +# This file is auto-generated by oagen. Do not edit. + +from .authorization_permission import AuthorizationPermission as AuthorizationPermission +from .create_authorization_permission import ( + CreateAuthorizationPermission as CreateAuthorizationPermission, +) +from .permission import Permission as Permission +from .permissions_order import PermissionsOrder as PermissionsOrder +from .update_authorization_permission import ( + UpdateAuthorizationPermission as UpdateAuthorizationPermission, +) diff --git a/src/workos/permissions/models/authorization_permission.py b/src/workos/permissions/models/authorization_permission.py new file mode 100644 index 00000000..c279baf0 --- /dev/null +++ b/src/workos/permissions/models/authorization_permission.py @@ -0,0 +1,77 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AuthorizationPermission: + """Authorization Permission model.""" + + object: Literal["permission"] + """Distinguishes the Permission object.""" + id: str + """Unique identifier of the Permission.""" + slug: str + """A unique key to reference the permission. Must be lowercase and contain only letters, numbers, hyphens, underscores, colons, periods, and asterisks.""" + name: str + """A descriptive name for the Permission.""" + description: Optional[str] + """An optional description of the Permission.""" + system: bool + """Whether the permission is a system permission. System permissions are managed by WorkOS and cannot be deleted.""" + resource_type_slug: str + """The slug of the resource type associated with the permission.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuthorizationPermission": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + slug=data["slug"], + name=data["name"], + description=data["description"], + system=data["system"], + resource_type_slug=data["resource_type_slug"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuthorizationPermission: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["slug"] = self.slug + result["name"] = self.name + if self.description is not None: + result["description"] = self.description + else: + result["description"] = None + result["system"] = self.system + result["resource_type_slug"] = self.resource_type_slug + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + return result diff --git a/src/workos/permissions/models/create_authorization_permission.py b/src/workos/permissions/models/create_authorization_permission.py new file mode 100644 index 00000000..c2d41cf8 --- /dev/null +++ b/src/workos/permissions/models/create_authorization_permission.py @@ -0,0 +1,49 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class CreateAuthorizationPermission: + """Create Authorization Permission model.""" + + slug: str + """A unique key to reference the permission. Must be lowercase and contain only letters, numbers, hyphens, underscores, colons, periods, and asterisks.""" + name: str + """A descriptive name for the Permission.""" + description: Optional[str] = None + """An optional description of the Permission.""" + resource_type_slug: Optional[str] = None + """The slug of the resource type this permission is scoped to.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CreateAuthorizationPermission": + """Deserialize from a dictionary.""" + try: + return cls( + slug=data["slug"], + name=data["name"], + description=data.get("description"), + resource_type_slug=data.get("resource_type_slug"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing CreateAuthorizationPermission: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["slug"] = self.slug + result["name"] = self.name + if self.description is not None: + result["description"] = self.description + else: + result["description"] = None + if self.resource_type_slug is not None: + result["resource_type_slug"] = self.resource_type_slug + return result diff --git a/src/workos/permissions/models/permission.py b/src/workos/permissions/models/permission.py new file mode 100644 index 00000000..22b98f9e --- /dev/null +++ b/src/workos/permissions/models/permission.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from .authorization_permission import AuthorizationPermission + +Permission = AuthorizationPermission diff --git a/src/workos/permissions/models/permissions_order.py b/src/workos/permissions/models/permissions_order.py new file mode 100644 index 00000000..b52ddff2 --- /dev/null +++ b/src/workos/permissions/models/permissions_order.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import ApplicationsOrder + +PermissionsOrder = ApplicationsOrder +__all__ = ["PermissionsOrder"] diff --git a/src/workos/permissions/models/update_authorization_permission.py b/src/workos/permissions/models/update_authorization_permission.py new file mode 100644 index 00000000..191ecb5f --- /dev/null +++ b/src/workos/permissions/models/update_authorization_permission.py @@ -0,0 +1,41 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class UpdateAuthorizationPermission: + """Update Authorization Permission model.""" + + name: Optional[str] = None + """A descriptive name for the Permission.""" + description: Optional[str] = None + """An optional description of the Permission.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "UpdateAuthorizationPermission": + """Deserialize from a dictionary.""" + try: + return cls( + name=data.get("name"), + description=data.get("description"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UpdateAuthorizationPermission: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + if self.name is not None: + result["name"] = self.name + if self.description is not None: + result["description"] = self.description + else: + result["description"] = None + return result diff --git a/src/workos/pipes/__init__.py b/src/workos/pipes/__init__.py new file mode 100644 index 00000000..07956777 --- /dev/null +++ b/src/workos/pipes/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import Pipes, AsyncPipes +from .models import * diff --git a/src/workos/pipes/_resource.py b/src/workos/pipes/_resource.py new file mode 100644 index 00000000..e574c004 --- /dev/null +++ b/src/workos/pipes/_resource.py @@ -0,0 +1,212 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Optional + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import ( + DataIntegrationAccessTokenResponse, + DataIntegrationAuthorizeUrlResponse, +) +from .._types import RequestOptions + + +class Pipes: + """Pipes API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def get_data_integration_authorize_url( + self, + slug: str, + *, + user_id: str, + organization_id: Optional[str] = None, + return_to: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> DataIntegrationAuthorizeUrlResponse: + """Get authorization URL + + Generates an OAuth authorization URL to initiate the connection flow for a user. Redirect the user to the returned URL to begin the OAuth flow with the third-party provider. + + Args: + slug: The slug identifier of the provider (e.g., `github`, `slack`, `notion`). + user_id: The ID of the user to authorize. + organization_id: An organization ID to scope the authorization to a specific organization. + return_to: The URL to redirect the user to after authorization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + DataIntegrationAuthorizeUrlResponse + + Raises: + BadRequestException: If the request is malformed (400). + AuthenticationException: If the API key is invalid (401). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "user_id": user_id, + "organization_id": organization_id, + "return_to": return_to, + }.items() + if v is not None + } + return self._client.request( + method="post", + path=f"data-integrations/{slug}/authorize", + body=body, + model=DataIntegrationAuthorizeUrlResponse, + request_options=request_options, + ) + + def get_userland_user_token( + self, + slug: str, + *, + user_id: str, + organization_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> DataIntegrationAccessTokenResponse: + """Get an access token for a connected account + + Fetches a valid OAuth access token for a user's connected account. WorkOS automatically handles token refresh, ensuring you always receive a valid, non-expired token. + + Args: + slug: The identifier of the integration. + user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier. + organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + DataIntegrationAccessTokenResponse + + Raises: + BadRequestException: If the request is malformed (400). + AuthenticationException: If the API key is invalid (401). + NotFoundException: If the resource is not found (404). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "user_id": user_id, + "organization_id": organization_id, + }.items() + if v is not None + } + return self._client.request( + method="post", + path=f"data-integrations/{slug}/token", + body=body, + model=DataIntegrationAccessTokenResponse, + request_options=request_options, + ) + + +class AsyncPipes: + """Pipes API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def get_data_integration_authorize_url( + self, + slug: str, + *, + user_id: str, + organization_id: Optional[str] = None, + return_to: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> DataIntegrationAuthorizeUrlResponse: + """Get authorization URL + + Generates an OAuth authorization URL to initiate the connection flow for a user. Redirect the user to the returned URL to begin the OAuth flow with the third-party provider. + + Args: + slug: The slug identifier of the provider (e.g., `github`, `slack`, `notion`). + user_id: The ID of the user to authorize. + organization_id: An organization ID to scope the authorization to a specific organization. + return_to: The URL to redirect the user to after authorization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + DataIntegrationAuthorizeUrlResponse + + Raises: + BadRequestException: If the request is malformed (400). + AuthenticationException: If the API key is invalid (401). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "user_id": user_id, + "organization_id": organization_id, + "return_to": return_to, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path=f"data-integrations/{slug}/authorize", + body=body, + model=DataIntegrationAuthorizeUrlResponse, + request_options=request_options, + ) + + async def get_userland_user_token( + self, + slug: str, + *, + user_id: str, + organization_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> DataIntegrationAccessTokenResponse: + """Get an access token for a connected account + + Fetches a valid OAuth access token for a user's connected account. WorkOS automatically handles token refresh, ensuring you always receive a valid, non-expired token. + + Args: + slug: The identifier of the integration. + user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier. + organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + DataIntegrationAccessTokenResponse + + Raises: + BadRequestException: If the request is malformed (400). + AuthenticationException: If the API key is invalid (401). + NotFoundException: If the resource is not found (404). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "user_id": user_id, + "organization_id": organization_id, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path=f"data-integrations/{slug}/token", + body=body, + model=DataIntegrationAccessTokenResponse, + request_options=request_options, + ) diff --git a/src/workos/pipes/models/__init__.py b/src/workos/pipes/models/__init__.py new file mode 100644 index 00000000..8ba64711 --- /dev/null +++ b/src/workos/pipes/models/__init__.py @@ -0,0 +1,14 @@ +# This file is auto-generated by oagen. Do not edit. + +from .data_integration_access_token_response import ( + DataIntegrationAccessTokenResponse as DataIntegrationAccessTokenResponse, +) +from .data_integration_authorize_url_response import ( + DataIntegrationAuthorizeUrlResponse as DataIntegrationAuthorizeUrlResponse, +) +from .data_integrations_get_data_integration_authorize_url_request import ( + DataIntegrationsGetDataIntegrationAuthorizeUrlRequest as DataIntegrationsGetDataIntegrationAuthorizeUrlRequest, +) +from .data_integrations_get_user_token_request import ( + DataIntegrationsGetUserTokenRequest as DataIntegrationsGetUserTokenRequest, +) diff --git a/src/workos/pipes/models/data_integration_access_token_response.py b/src/workos/pipes/models/data_integration_access_token_response.py new file mode 100644 index 00000000..95b481ee --- /dev/null +++ b/src/workos/pipes/models/data_integration_access_token_response.py @@ -0,0 +1,29 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class DataIntegrationAccessTokenResponse: + """Data Integration Access Token Response model.""" + + pass + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "DataIntegrationAccessTokenResponse": + """Deserialize from a dictionary.""" + try: + return cls() + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing DataIntegrationAccessTokenResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + return result diff --git a/src/workos/pipes/models/data_integration_authorize_url_response.py b/src/workos/pipes/models/data_integration_authorize_url_response.py new file mode 100644 index 00000000..238d3b5a --- /dev/null +++ b/src/workos/pipes/models/data_integration_authorize_url_response.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class DataIntegrationAuthorizeUrlResponse: + """Data Integration Authorize Url Response model.""" + + url: str + """The OAuth authorization URL to redirect the user to.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "DataIntegrationAuthorizeUrlResponse": + """Deserialize from a dictionary.""" + try: + return cls( + url=data["url"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing DataIntegrationAuthorizeUrlResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["url"] = self.url + return result diff --git a/src/workos/pipes/models/data_integrations_get_data_integration_authorize_url_request.py b/src/workos/pipes/models/data_integrations_get_data_integration_authorize_url_request.py new file mode 100644 index 00000000..f111882a --- /dev/null +++ b/src/workos/pipes/models/data_integrations_get_data_integration_authorize_url_request.py @@ -0,0 +1,45 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class DataIntegrationsGetDataIntegrationAuthorizeUrlRequest: + """Data Integrations Get Data Integration Authorize Url Request model.""" + + user_id: str + """The ID of the user to authorize.""" + organization_id: Optional[str] = None + """An organization ID to scope the authorization to a specific organization.""" + return_to: Optional[str] = None + """The URL to redirect the user to after authorization.""" + + @classmethod + def from_dict( + cls, data: Dict[str, Any] + ) -> "DataIntegrationsGetDataIntegrationAuthorizeUrlRequest": + """Deserialize from a dictionary.""" + try: + return cls( + user_id=data["user_id"], + organization_id=data.get("organization_id"), + return_to=data.get("return_to"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing DataIntegrationsGetDataIntegrationAuthorizeUrlRequest: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["user_id"] = self.user_id + if self.organization_id is not None: + result["organization_id"] = self.organization_id + if self.return_to is not None: + result["return_to"] = self.return_to + return result diff --git a/src/workos/pipes/models/data_integrations_get_user_token_request.py b/src/workos/pipes/models/data_integrations_get_user_token_request.py new file mode 100644 index 00000000..97b8a870 --- /dev/null +++ b/src/workos/pipes/models/data_integrations_get_user_token_request.py @@ -0,0 +1,38 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class DataIntegrationsGetUserTokenRequest: + """Data Integrations Get User Token Request model.""" + + user_id: str + """A [User](https://workos.com/docs/reference/authkit/user) identifier.""" + organization_id: Optional[str] = None + """An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to scope the connection to a specific organization.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "DataIntegrationsGetUserTokenRequest": + """Deserialize from a dictionary.""" + try: + return cls( + user_id=data["user_id"], + organization_id=data.get("organization_id"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing DataIntegrationsGetUserTokenRequest: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["user_id"] = self.user_id + if self.organization_id is not None: + result["organization_id"] = self.organization_id + return result diff --git a/src/workos/radar/__init__.py b/src/workos/radar/__init__.py new file mode 100644 index 00000000..fe581cf6 --- /dev/null +++ b/src/workos/radar/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import Radar, AsyncRadar +from .models import * diff --git a/src/workos/radar/_resource.py b/src/workos/radar/_resource.py new file mode 100644 index 00000000..e60c29f9 --- /dev/null +++ b/src/workos/radar/_resource.py @@ -0,0 +1,368 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Literal, Optional + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import RadarListEntryAlreadyPresentResponse, RadarStandaloneResponse +from .models import RadarAction, RadarType +from workos.common.models import ( + RadarStandaloneAssessRequestAction, + RadarStandaloneAssessRequestAuthMethod, +) +from .._types import RequestOptions + + +class Radar: + """Radar API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def assess( + self, + *, + ip_address: str, + user_agent: str, + email: str, + auth_method: RadarStandaloneAssessRequestAuthMethod, + action: RadarStandaloneAssessRequestAction, + device_fingerprint: Optional[str] = None, + bot_score: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> RadarStandaloneResponse: + """Create an attempt + + Assess a request for risk using the Radar engine and receive a verdict. + + Args: + ip_address: The IP address of the request to assess. + user_agent: The user agent string of the request to assess. + email: The email address of the user making the request. + auth_method: The authentication method being used. + action: The action being performed. + device_fingerprint: An optional device fingerprint for the request. + bot_score: An optional bot detection score for the request. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + RadarStandaloneResponse + + Raises: + BadRequestException: If the request is malformed (400). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "ip_address": ip_address, + "user_agent": user_agent, + "email": email, + "auth_method": auth_method, + "action": action, + "device_fingerprint": device_fingerprint, + "bot_score": bot_score, + }.items() + if v is not None + } + return self._client.request( + method="post", + path="radar/attempts", + body=body, + model=RadarStandaloneResponse, + request_options=request_options, + ) + + def update_radar_attempt( + self, + id: str, + *, + challenge_status: Optional[Literal["success"]] = None, + attempt_status: Optional[Literal["success"]] = None, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Update a Radar attempt + + You may optionally inform Radar that an authentication attempt or challenge was successful using this endpoint. Some Radar controls depend on tracking recent successful attempts, such as impossible travel. + + Args: + id: The unique identifier of the Radar attempt to update. + challenge_status: Set to `"success"` to mark the challenge as completed. + attempt_status: Set to `"success"` to mark the authentication attempt as successful. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "challenge_status": challenge_status, + "attempt_status": attempt_status, + }.items() + if v is not None + } + self._client.request( + method="put", + path=f"radar/attempts/{id}", + body=body, + request_options=request_options, + ) + + def update_radar_list( + self, + type: RadarType, + action: RadarAction, + *, + entry: str, + request_options: Optional[RequestOptions] = None, + ) -> RadarListEntryAlreadyPresentResponse: + """Add an entry to a Radar list + + Add an entry to a Radar list. + + Args: + type: The type of the Radar list (e.g. ip_address, domain, email). + action: The list action indicating whether to add the entry to the allow or block list. + entry: The value to add to the list. Must match the format of the list type (e.g. a valid IP address for `ip_address`, a valid email for `email`). + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + RadarListEntryAlreadyPresentResponse + + Raises: + BadRequestException: If the request is malformed (400). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "entry": entry, + } + return self._client.request( + method="post", + path=f"radar/lists/{type}/{action}", + body=body, + model=RadarListEntryAlreadyPresentResponse, + request_options=request_options, + ) + + def delete_radar_list_entry( + self, + type: RadarType, + action: RadarAction, + *, + entry: str, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Remove an entry from a Radar list + + Remove an entry from a Radar list. + + Args: + type: The type of the Radar list (e.g. ip_address, domain, email). + action: The list action indicating whether to remove the entry from the allow or block list. + entry: The value to remove from the list. Must match an existing entry. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "entry": entry, + } + self._client.request( + method="delete", + path=f"radar/lists/{type}/{action}", + body=body, + request_options=request_options, + ) + + +class AsyncRadar: + """Radar API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def assess( + self, + *, + ip_address: str, + user_agent: str, + email: str, + auth_method: RadarStandaloneAssessRequestAuthMethod, + action: RadarStandaloneAssessRequestAction, + device_fingerprint: Optional[str] = None, + bot_score: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> RadarStandaloneResponse: + """Create an attempt + + Assess a request for risk using the Radar engine and receive a verdict. + + Args: + ip_address: The IP address of the request to assess. + user_agent: The user agent string of the request to assess. + email: The email address of the user making the request. + auth_method: The authentication method being used. + action: The action being performed. + device_fingerprint: An optional device fingerprint for the request. + bot_score: An optional bot detection score for the request. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + RadarStandaloneResponse + + Raises: + BadRequestException: If the request is malformed (400). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "ip_address": ip_address, + "user_agent": user_agent, + "email": email, + "auth_method": auth_method, + "action": action, + "device_fingerprint": device_fingerprint, + "bot_score": bot_score, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path="radar/attempts", + body=body, + model=RadarStandaloneResponse, + request_options=request_options, + ) + + async def update_radar_attempt( + self, + id: str, + *, + challenge_status: Optional[Literal["success"]] = None, + attempt_status: Optional[Literal["success"]] = None, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Update a Radar attempt + + You may optionally inform Radar that an authentication attempt or challenge was successful using this endpoint. Some Radar controls depend on tracking recent successful attempts, such as impossible travel. + + Args: + id: The unique identifier of the Radar attempt to update. + challenge_status: Set to `"success"` to mark the challenge as completed. + attempt_status: Set to `"success"` to mark the authentication attempt as successful. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "challenge_status": challenge_status, + "attempt_status": attempt_status, + }.items() + if v is not None + } + await self._client.request( + method="put", + path=f"radar/attempts/{id}", + body=body, + request_options=request_options, + ) + + async def update_radar_list( + self, + type: RadarType, + action: RadarAction, + *, + entry: str, + request_options: Optional[RequestOptions] = None, + ) -> RadarListEntryAlreadyPresentResponse: + """Add an entry to a Radar list + + Add an entry to a Radar list. + + Args: + type: The type of the Radar list (e.g. ip_address, domain, email). + action: The list action indicating whether to add the entry to the allow or block list. + entry: The value to add to the list. Must match the format of the list type (e.g. a valid IP address for `ip_address`, a valid email for `email`). + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + RadarListEntryAlreadyPresentResponse + + Raises: + BadRequestException: If the request is malformed (400). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "entry": entry, + } + return await self._client.request( + method="post", + path=f"radar/lists/{type}/{action}", + body=body, + model=RadarListEntryAlreadyPresentResponse, + request_options=request_options, + ) + + async def delete_radar_list_entry( + self, + type: RadarType, + action: RadarAction, + *, + entry: str, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Remove an entry from a Radar list + + Remove an entry from a Radar list. + + Args: + type: The type of the Radar list (e.g. ip_address, domain, email). + action: The list action indicating whether to remove the entry from the allow or block list. + entry: The value to remove from the list. Must match an existing entry. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "entry": entry, + } + await self._client.request( + method="delete", + path=f"radar/lists/{type}/{action}", + body=body, + request_options=request_options, + ) diff --git a/src/workos/radar/models/__init__.py b/src/workos/radar/models/__init__.py new file mode 100644 index 00000000..afcc8cec --- /dev/null +++ b/src/workos/radar/models/__init__.py @@ -0,0 +1,22 @@ +# This file is auto-generated by oagen. Do not edit. + +from .radar_action import RadarAction as RadarAction +from .radar_list_entry_already_present_response import ( + RadarListEntryAlreadyPresentResponse as RadarListEntryAlreadyPresentResponse, +) +from .radar_standalone_assess_request import ( + RadarStandaloneAssessRequest as RadarStandaloneAssessRequest, +) +from .radar_standalone_delete_radar_list_entry_request import ( + RadarStandaloneDeleteRadarListEntryRequest as RadarStandaloneDeleteRadarListEntryRequest, +) +from .radar_standalone_response import ( + RadarStandaloneResponse as RadarStandaloneResponse, +) +from .radar_standalone_update_radar_attempt_request import ( + RadarStandaloneUpdateRadarAttemptRequest as RadarStandaloneUpdateRadarAttemptRequest, +) +from .radar_standalone_update_radar_list_request import ( + RadarStandaloneUpdateRadarListRequest as RadarStandaloneUpdateRadarListRequest, +) +from .radar_type import RadarType as RadarType diff --git a/src/workos/radar/models/radar_action.py b/src/workos/radar/models/radar_action.py new file mode 100644 index 00000000..b145ec6b --- /dev/null +++ b/src/workos/radar/models/radar_action.py @@ -0,0 +1,28 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of radar action values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class RadarAction(str, Enum): + """Known values for RadarAction.""" + + BLOCK = "block" + ALLOW = "allow" + + @classmethod + def _missing_(cls, value: object) -> Optional["RadarAction"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +RadarActionLiteral: TypeAlias = Literal["block", "allow"] diff --git a/src/workos/radar/models/radar_list_entry_already_present_response.py b/src/workos/radar/models/radar_list_entry_already_present_response.py new file mode 100644 index 00000000..05110fe3 --- /dev/null +++ b/src/workos/radar/models/radar_list_entry_already_present_response.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class RadarListEntryAlreadyPresentResponse: + """Radar List Entry Already Present Response model.""" + + message: str + """A message indicating the entry already exists.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "RadarListEntryAlreadyPresentResponse": + """Deserialize from a dictionary.""" + try: + return cls( + message=data["message"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing RadarListEntryAlreadyPresentResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["message"] = self.message + return result diff --git a/src/workos/radar/models/radar_standalone_assess_request.py b/src/workos/radar/models/radar_standalone_assess_request.py new file mode 100644 index 00000000..305ec447 --- /dev/null +++ b/src/workos/radar/models/radar_standalone_assess_request.py @@ -0,0 +1,61 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException +from workos.common.models import RadarStandaloneAssessRequestAction +from workos.common.models import RadarStandaloneAssessRequestAuthMethod + + +@dataclass(slots=True) +class RadarStandaloneAssessRequest: + """Radar Standalone Assess Request model.""" + + ip_address: str + """The IP address of the request to assess.""" + user_agent: str + """The user agent string of the request to assess.""" + email: str + """The email address of the user making the request.""" + auth_method: "RadarStandaloneAssessRequestAuthMethod" + """The authentication method being used.""" + action: "RadarStandaloneAssessRequestAction" + """The action being performed.""" + device_fingerprint: Optional[str] = None + """An optional device fingerprint for the request.""" + bot_score: Optional[str] = None + """An optional bot detection score for the request.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "RadarStandaloneAssessRequest": + """Deserialize from a dictionary.""" + try: + return cls( + ip_address=data["ip_address"], + user_agent=data["user_agent"], + email=data["email"], + auth_method=RadarStandaloneAssessRequestAuthMethod(data["auth_method"]), + action=RadarStandaloneAssessRequestAction(data["action"]), + device_fingerprint=data.get("device_fingerprint"), + bot_score=data.get("bot_score"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing RadarStandaloneAssessRequest: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["ip_address"] = self.ip_address + result["user_agent"] = self.user_agent + result["email"] = self.email + result["auth_method"] = self.auth_method + result["action"] = self.action + if self.device_fingerprint is not None: + result["device_fingerprint"] = self.device_fingerprint + if self.bot_score is not None: + result["bot_score"] = self.bot_score + return result diff --git a/src/workos/radar/models/radar_standalone_delete_radar_list_entry_request.py b/src/workos/radar/models/radar_standalone_delete_radar_list_entry_request.py new file mode 100644 index 00000000..ba08f1f7 --- /dev/null +++ b/src/workos/radar/models/radar_standalone_delete_radar_list_entry_request.py @@ -0,0 +1,35 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class RadarStandaloneDeleteRadarListEntryRequest: + """Radar Standalone Delete Radar List Entry Request model.""" + + entry: str + """The value to remove from the list. Must match an existing entry.""" + + @classmethod + def from_dict( + cls, data: Dict[str, Any] + ) -> "RadarStandaloneDeleteRadarListEntryRequest": + """Deserialize from a dictionary.""" + try: + return cls( + entry=data["entry"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing RadarStandaloneDeleteRadarListEntryRequest: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["entry"] = self.entry + return result diff --git a/src/workos/radar/models/radar_standalone_response.py b/src/workos/radar/models/radar_standalone_response.py new file mode 100644 index 00000000..d7a1f183 --- /dev/null +++ b/src/workos/radar/models/radar_standalone_response.py @@ -0,0 +1,58 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException +from workos.common.models import RadarStandaloneResponseBlocklistType +from workos.common.models import RadarStandaloneResponseControl +from workos.common.models import RadarStandaloneResponseVerdict + + +@dataclass(slots=True) +class RadarStandaloneResponse: + """Radar Standalone Response model.""" + + verdict: "RadarStandaloneResponseVerdict" + """The verdict of the risk assessment.""" + reason: str + """A human-readable reason for the verdict.""" + attempt_id: str + """Unique identifier of the authentication attempt.""" + control: Optional["RadarStandaloneResponseControl"] = None + """The Radar control that triggered the verdict. Only present if the verdict is `block` or `challenge`.""" + blocklist_type: Optional["RadarStandaloneResponseBlocklistType"] = None + """The type of blocklist entry that triggered the verdict. Only present if the control is `restriction`.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "RadarStandaloneResponse": + """Deserialize from a dictionary.""" + try: + return cls( + verdict=RadarStandaloneResponseVerdict(data["verdict"]), + reason=data["reason"], + attempt_id=data["attempt_id"], + control=RadarStandaloneResponseControl(_v) + if (_v := data.get("control")) is not None + else None, + blocklist_type=RadarStandaloneResponseBlocklistType(_v) + if (_v := data.get("blocklist_type")) is not None + else None, + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing RadarStandaloneResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["verdict"] = self.verdict + result["reason"] = self.reason + result["attempt_id"] = self.attempt_id + if self.control is not None: + result["control"] = self.control + if self.blocklist_type is not None: + result["blocklist_type"] = self.blocklist_type + return result diff --git a/src/workos/radar/models/radar_standalone_update_radar_attempt_request.py b/src/workos/radar/models/radar_standalone_update_radar_attempt_request.py new file mode 100644 index 00000000..ce93f58b --- /dev/null +++ b/src/workos/radar/models/radar_standalone_update_radar_attempt_request.py @@ -0,0 +1,41 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class RadarStandaloneUpdateRadarAttemptRequest: + """Radar Standalone Update Radar Attempt Request model.""" + + challenge_status: Optional[Literal["success"]] = None + """Set to `"success"` to mark the challenge as completed.""" + attempt_status: Optional[Literal["success"]] = None + """Set to `"success"` to mark the authentication attempt as successful.""" + + @classmethod + def from_dict( + cls, data: Dict[str, Any] + ) -> "RadarStandaloneUpdateRadarAttemptRequest": + """Deserialize from a dictionary.""" + try: + return cls( + challenge_status=data.get("challenge_status"), + attempt_status=data.get("attempt_status"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing RadarStandaloneUpdateRadarAttemptRequest: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + if self.challenge_status is not None: + result["challenge_status"] = self.challenge_status + if self.attempt_status is not None: + result["attempt_status"] = self.attempt_status + return result diff --git a/src/workos/radar/models/radar_standalone_update_radar_list_request.py b/src/workos/radar/models/radar_standalone_update_radar_list_request.py new file mode 100644 index 00000000..6e7b2280 --- /dev/null +++ b/src/workos/radar/models/radar_standalone_update_radar_list_request.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class RadarStandaloneUpdateRadarListRequest: + """Radar Standalone Update Radar List Request model.""" + + entry: str + """The value to add to the list. Must match the format of the list type (e.g. a valid IP address for `ip_address`, a valid email for `email`).""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "RadarStandaloneUpdateRadarListRequest": + """Deserialize from a dictionary.""" + try: + return cls( + entry=data["entry"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing RadarStandaloneUpdateRadarListRequest: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["entry"] = self.entry + return result diff --git a/src/workos/radar/models/radar_type.py b/src/workos/radar/models/radar_type.py new file mode 100644 index 00000000..4b464c2f --- /dev/null +++ b/src/workos/radar/models/radar_type.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.common.models import RadarStandaloneResponseBlocklistType + +RadarType = RadarStandaloneResponseBlocklistType +__all__ = ["RadarType"] diff --git a/src/workos/sso/__init__.py b/src/workos/sso/__init__.py new file mode 100644 index 00000000..f8787723 --- /dev/null +++ b/src/workos/sso/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import SSO, AsyncSSO +from .models import * diff --git a/src/workos/sso/_resource.py b/src/workos/sso/_resource.py new file mode 100644 index 00000000..0a628cc1 --- /dev/null +++ b/src/workos/sso/_resource.py @@ -0,0 +1,642 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import Profile, SSOLogoutAuthorizeResponse, SSOTokenResponse +from .models import SSOProvider +from ..connections.models import Connection, ConnectionsConnectionType, ConnectionsOrder +from .._pagination import AsyncPage, SyncPage +from .._types import RequestOptions + + +class SSO: + """SSO API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def authorize( + self, + *, + provider_scopes: Optional[List[str]] = None, + provider_query_params: Optional[Dict[str, str]] = None, + client_id: str, + domain: Optional[str] = None, + provider: Optional[SSOProvider] = None, + redirect_uri: str, + response_type: Literal["code"], + state: Optional[str] = None, + connection: Optional[str] = None, + organization: Optional[str] = None, + domain_hint: Optional[str] = None, + login_hint: Optional[str] = None, + nonce: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> str: + """Initiate SSO + + Initiates the single sign-on flow. + + Args: + provider_scopes: Additional OAuth scopes to request from the identity provider. Only applicable when using OAuth connections. + provider_query_params: Key/value pairs of query parameters to pass to the OAuth provider. Only applicable when using OAuth connections. + client_id: The unique identifier of the WorkOS environment client. + domain: Deprecated. Use `connection` or `organization` instead. Used to initiate SSO for a connection by domain. The domain must be associated with a connection in your WorkOS environment. + provider: Used to initiate OAuth authentication with Google, Microsoft, GitHub, or Apple. + redirect_uri: Where to redirect the user after they complete the authentication process. You must use one of the redirect URIs configured via the [Redirects](https://dashboard.workos.com/redirects) page on the dashboard. + response_type: The only valid option for the response type parameter is `"code"`. + + The `"code"` parameter value initiates an [authorization code grant type](https://tools.ietf.org/html/rfc6749#section-4.1). This grant type allows you to exchange an authorization code for an access token during the redirect that takes place after a user has authenticated with an identity provider. + state: An optional parameter that can be used to encode arbitrary information to help restore application state between redirects. If included, the redirect URI received from WorkOS will contain the exact `state` that was passed. + connection: Used to initiate SSO for a connection. The value should be a WorkOS connection ID. + + You can persist the WorkOS connection ID with application user or team identifiers. WorkOS will use the connection indicated by the connection parameter to direct the user to the corresponding IdP for authentication. + organization: Used to initiate SSO for an organization. The value should be a WorkOS organization ID. + + You can persist the WorkOS organization ID with application user or team identifiers. WorkOS will use the organization ID to determine the appropriate connection and the IdP to direct the user to for authentication. + domain_hint: Can be used to pre-fill the domain field when initiating authentication with Microsoft OAuth or with a Google SAML connection type. + login_hint: Can be used to pre-fill the username/email address field of the IdP sign-in page for the user, if you know their username ahead of time. Currently supported for OAuth, OpenID Connect, Okta, and Entra ID connections. + nonce: A random string generated by the client that is used to mitigate replay attacks. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + str + + Raises: + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "provider_scopes": provider_scopes, + "provider_query_params": provider_query_params, + "client_id": client_id, + "domain": domain, + "provider": provider.value if provider else None, + "redirect_uri": redirect_uri, + "response_type": response_type, + "state": state, + "connection": connection, + "organization": organization, + "domain_hint": domain_hint, + "login_hint": login_hint, + "nonce": nonce, + }.items() + if v is not None + } + return self._client.build_url("sso/authorize", params) + + def logout( + self, + *, + token: str, + request_options: Optional[RequestOptions] = None, + ) -> str: + """Logout Redirect + + Logout allows to sign out a user from your application by triggering the identity provider sign out flow. This `GET` endpoint should be a redirection, since the identity provider user will be identified in the browser session. + + Before redirecting to this endpoint, you need to generate a short-lived logout token using the [Logout Authorize](https://workos.com/docs/reference/sso/logout/authorize) endpoint. + + Args: + token: The logout token returned from the [Logout Authorize](https://workos.com/docs/reference/sso/logout/authorize) endpoint. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + str + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "token": token, + }.items() + if v is not None + } + return self._client.build_url("sso/logout", params) + + def logout_authorize( + self, + *, + profile_id: str, + request_options: Optional[RequestOptions] = None, + ) -> SSOLogoutAuthorizeResponse: + """Logout Authorize + + You should call this endpoint from your server to generate a logout token which is required for the [Logout Redirect](https://workos.com/docs/reference/sso/logout) endpoint. + + Args: + profile_id: The unique ID of the profile to log out. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SSOLogoutAuthorizeResponse + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "profile_id": profile_id, + } + return self._client.request( + method="post", + path="sso/logout/authorize", + body=body, + model=SSOLogoutAuthorizeResponse, + request_options=request_options, + ) + + def get_profile( + self, + *, + access_token: str, + request_options: Optional[RequestOptions] = None, + ) -> Profile: + """Get a User Profile + + Exchange an access token for a user's [Profile](https://workos.com/docs/reference/sso/profile). Because this profile is returned in the [Get a Profile and Token endpoint](https://workos.com/docs/reference/sso/profile/get-profile-and-token) your application usually does not need to call this endpoint. It is available for any authentication flows that require an additional endpoint to retrieve a user's profile. + + Args: + access_token: The bearer token for authentication. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Profile + + Raises: + AuthenticationException: If the API key is invalid (401). + NotFoundException: If the resource is not found (404). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + request_options = request_options or {} + request_options = { + **request_options, + "extra_headers": { + **(request_options.get("extra_headers") or {}), + "Authorization": f"Bearer {access_token}", + }, + } + return self._client.request( + method="get", + path="sso/profile", + model=Profile, + request_options=request_options, + ) + + def get_profile_and_token( + self, + *, + client_id: str, + client_secret: str, + code: str, + grant_type: Literal["authorization_code"], + request_options: Optional[RequestOptions] = None, + ) -> SSOTokenResponse: + """Get a Profile and Token + + Get an access token along with the user [Profile](https://workos.com/docs/reference/sso/profile) using the code passed to your [Redirect URI](https://workos.com/docs/reference/sso/get-authorization-url/redirect-uri). + + Args: + client_id: The client ID of the WorkOS environment. + client_secret: The client secret of the WorkOS environment. + code: The authorization code received from the authorization callback. + grant_type: The grant type for the token request. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SSOTokenResponse + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "client_id": client_id, + "client_secret": client_secret, + "code": code, + "grant_type": grant_type, + } + return self._client.request( + method="post", + path="sso/token", + body=body, + model=SSOTokenResponse, + request_options=request_options, + ) + + token = get_profile_and_token + + # @oagen-ignore-start + def list_connections( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[ConnectionsOrder] = None, + connection_type: Optional[ConnectionsConnectionType] = None, + domain: Optional[str] = None, + organization_id: Optional[str] = None, + search: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[Connection]: + """List Connections + + Get a list of all of your existing connections matching the criteria specified. + + Args: + connection_type: Filter Connections by their type. + domain: Filter Connections by their associated domain. + organization_id: Filter Connections by their associated organization. + search: Searchable text to match against Connection names. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[Connection] + """ + return self._client.connections.list( + limit=limit, + before=before, + after=after, + order=order, + connection_type=connection_type, + domain=domain, + organization_id=organization_id, + search=search, + request_options=request_options, + ) + + def get_connection( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Connection: + """Get a Connection + + Get the details of an existing connection. + + Args: + id: Unique identifier for the Connection. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Connection + """ + return self._client.connections.get(id, request_options=request_options) + + def delete_connection( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a Connection + + Permanently deletes an existing connection. It cannot be undone. + + Args: + id: Unique identifier for the Connection. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + """ + self._client.connections.delete(id, request_options=request_options) + # @oagen-ignore-end + + +class AsyncSSO: + """SSO API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def authorize( + self, + *, + provider_scopes: Optional[List[str]] = None, + provider_query_params: Optional[Dict[str, str]] = None, + client_id: str, + domain: Optional[str] = None, + provider: Optional[SSOProvider] = None, + redirect_uri: str, + response_type: Literal["code"], + state: Optional[str] = None, + connection: Optional[str] = None, + organization: Optional[str] = None, + domain_hint: Optional[str] = None, + login_hint: Optional[str] = None, + nonce: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> str: + """Initiate SSO + + Initiates the single sign-on flow. + + Args: + provider_scopes: Additional OAuth scopes to request from the identity provider. Only applicable when using OAuth connections. + provider_query_params: Key/value pairs of query parameters to pass to the OAuth provider. Only applicable when using OAuth connections. + client_id: The unique identifier of the WorkOS environment client. + domain: Deprecated. Use `connection` or `organization` instead. Used to initiate SSO for a connection by domain. The domain must be associated with a connection in your WorkOS environment. + provider: Used to initiate OAuth authentication with Google, Microsoft, GitHub, or Apple. + redirect_uri: Where to redirect the user after they complete the authentication process. You must use one of the redirect URIs configured via the [Redirects](https://dashboard.workos.com/redirects) page on the dashboard. + response_type: The only valid option for the response type parameter is `"code"`. + + The `"code"` parameter value initiates an [authorization code grant type](https://tools.ietf.org/html/rfc6749#section-4.1). This grant type allows you to exchange an authorization code for an access token during the redirect that takes place after a user has authenticated with an identity provider. + state: An optional parameter that can be used to encode arbitrary information to help restore application state between redirects. If included, the redirect URI received from WorkOS will contain the exact `state` that was passed. + connection: Used to initiate SSO for a connection. The value should be a WorkOS connection ID. + + You can persist the WorkOS connection ID with application user or team identifiers. WorkOS will use the connection indicated by the connection parameter to direct the user to the corresponding IdP for authentication. + organization: Used to initiate SSO for an organization. The value should be a WorkOS organization ID. + + You can persist the WorkOS organization ID with application user or team identifiers. WorkOS will use the organization ID to determine the appropriate connection and the IdP to direct the user to for authentication. + domain_hint: Can be used to pre-fill the domain field when initiating authentication with Microsoft OAuth or with a Google SAML connection type. + login_hint: Can be used to pre-fill the username/email address field of the IdP sign-in page for the user, if you know their username ahead of time. Currently supported for OAuth, OpenID Connect, Okta, and Entra ID connections. + nonce: A random string generated by the client that is used to mitigate replay attacks. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + str + + Raises: + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "provider_scopes": provider_scopes, + "provider_query_params": provider_query_params, + "client_id": client_id, + "domain": domain, + "provider": provider.value if provider else None, + "redirect_uri": redirect_uri, + "response_type": response_type, + "state": state, + "connection": connection, + "organization": organization, + "domain_hint": domain_hint, + "login_hint": login_hint, + "nonce": nonce, + }.items() + if v is not None + } + return self._client.build_url("sso/authorize", params) + + async def logout( + self, + *, + token: str, + request_options: Optional[RequestOptions] = None, + ) -> str: + """Logout Redirect + + Logout allows to sign out a user from your application by triggering the identity provider sign out flow. This `GET` endpoint should be a redirection, since the identity provider user will be identified in the browser session. + + Before redirecting to this endpoint, you need to generate a short-lived logout token using the [Logout Authorize](https://workos.com/docs/reference/sso/logout/authorize) endpoint. + + Args: + token: The logout token returned from the [Logout Authorize](https://workos.com/docs/reference/sso/logout/authorize) endpoint. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + str + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "token": token, + }.items() + if v is not None + } + return self._client.build_url("sso/logout", params) + + async def logout_authorize( + self, + *, + profile_id: str, + request_options: Optional[RequestOptions] = None, + ) -> SSOLogoutAuthorizeResponse: + """Logout Authorize + + You should call this endpoint from your server to generate a logout token which is required for the [Logout Redirect](https://workos.com/docs/reference/sso/logout) endpoint. + + Args: + profile_id: The unique ID of the profile to log out. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SSOLogoutAuthorizeResponse + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "profile_id": profile_id, + } + return await self._client.request( + method="post", + path="sso/logout/authorize", + body=body, + model=SSOLogoutAuthorizeResponse, + request_options=request_options, + ) + + async def get_profile( + self, + *, + access_token: str, + request_options: Optional[RequestOptions] = None, + ) -> Profile: + """Get a User Profile + + Exchange an access token for a user's [Profile](https://workos.com/docs/reference/sso/profile). Because this profile is returned in the [Get a Profile and Token endpoint](https://workos.com/docs/reference/sso/profile/get-profile-and-token) your application usually does not need to call this endpoint. It is available for any authentication flows that require an additional endpoint to retrieve a user's profile. + + Args: + access_token: The bearer token for authentication. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Profile + + Raises: + AuthenticationException: If the API key is invalid (401). + NotFoundException: If the resource is not found (404). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + request_options = request_options or {} + request_options = { + **request_options, + "extra_headers": { + **(request_options.get("extra_headers") or {}), + "Authorization": f"Bearer {access_token}", + }, + } + return await self._client.request( + method="get", + path="sso/profile", + model=Profile, + request_options=request_options, + ) + + async def get_profile_and_token( + self, + *, + client_id: str, + client_secret: str, + code: str, + grant_type: Literal["authorization_code"], + request_options: Optional[RequestOptions] = None, + ) -> SSOTokenResponse: + """Get a Profile and Token + + Get an access token along with the user [Profile](https://workos.com/docs/reference/sso/profile) using the code passed to your [Redirect URI](https://workos.com/docs/reference/sso/get-authorization-url/redirect-uri). + + Args: + client_id: The client ID of the WorkOS environment. + client_secret: The client secret of the WorkOS environment. + code: The authorization code received from the authorization callback. + grant_type: The grant type for the token request. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SSOTokenResponse + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "client_id": client_id, + "client_secret": client_secret, + "code": code, + "grant_type": grant_type, + } + return await self._client.request( + method="post", + path="sso/token", + body=body, + model=SSOTokenResponse, + request_options=request_options, + ) + + token = get_profile_and_token + + # @oagen-ignore-start + async def list_connections( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[ConnectionsOrder] = None, + connection_type: Optional[ConnectionsConnectionType] = None, + domain: Optional[str] = None, + organization_id: Optional[str] = None, + search: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[Connection]: + """List Connections + + Get a list of all of your existing connections matching the criteria specified. + + Args: + connection_type: Filter Connections by their type. + domain: Filter Connections by their associated domain. + organization_id: Filter Connections by their associated organization. + search: Searchable text to match against Connection names. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[Connection] + """ + return await self._client.connections.list( + limit=limit, + before=before, + after=after, + order=order, + connection_type=connection_type, + domain=domain, + organization_id=organization_id, + search=search, + request_options=request_options, + ) + + async def get_connection( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Connection: + """Get a Connection + + Get the details of an existing connection. + + Args: + id: Unique identifier for the Connection. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Connection + """ + return await self._client.connections.get(id, request_options=request_options) + + async def delete_connection( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a Connection + + Permanently deletes an existing connection. It cannot be undone. + + Args: + id: Unique identifier for the Connection. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + """ + await self._client.connections.delete(id, request_options=request_options) + # @oagen-ignore-end diff --git a/src/workos/sso/models/__init__.py b/src/workos/sso/models/__init__.py new file mode 100644 index 00000000..a5b8c15d --- /dev/null +++ b/src/workos/sso/models/__init__.py @@ -0,0 +1,18 @@ +# This file is auto-generated by oagen. Do not edit. + +from .profile import Profile as Profile +from .sso_authorize_url_response import ( + SSOAuthorizeUrlResponse as SSOAuthorizeUrlResponse, +) +from .sso_logout_authorize_request import ( + SSOLogoutAuthorizeRequest as SSOLogoutAuthorizeRequest, +) +from .sso_logout_authorize_response import ( + SSOLogoutAuthorizeResponse as SSOLogoutAuthorizeResponse, +) +from .sso_provider import SSOProvider as SSOProvider +from .sso_token_response import SSOTokenResponse as SSOTokenResponse +from .sso_token_response_oauth_token import ( + SSOTokenResponseOAuthToken as SSOTokenResponseOAuthToken, +) +from .token_query import TokenQuery as TokenQuery diff --git a/src/workos/sso/models/profile.py b/src/workos/sso/models/profile.py new file mode 100644 index 00000000..67d52e20 --- /dev/null +++ b/src/workos/sso/models/profile.py @@ -0,0 +1,113 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, List, Literal, Optional +from workos._errors import BaseRequestException + +from workos.authorization.models import SlimRole +from workos.common.models import ProfileConnectionType + + +@dataclass(slots=True) +class Profile: + """The user profile returned by the identity provider.""" + + object: Literal["profile"] + """Distinguishes the profile object.""" + id: str + """Unique identifier of the profile.""" + organization_id: Optional[str] + """The ID of the organization the user belongs to.""" + connection_id: str + """The ID of the SSO connection used for authentication.""" + connection_type: "ProfileConnectionType" + """The type of SSO connection.""" + idp_id: str + """The user's unique identifier from the identity provider.""" + email: str + """The user's email address.""" + first_name: Optional[str] + """The user's first name.""" + last_name: Optional[str] + """The user's last name.""" + raw_attributes: Dict[str, Any] + """The complete set of raw attributes returned by the identity provider.""" + role: Optional["SlimRole"] = None + """The role assigned to the user within the organization, if applicable.""" + roles: Optional[List["SlimRole"]] = None + """The roles assigned to the user within the organization, if applicable.""" + groups: Optional[List[str]] = None + """The groups the user belongs to, as returned by the identity provider.""" + custom_attributes: Optional[Dict[str, Any]] = None + """Custom attribute mappings defined for the connection, returned as key-value pairs.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Profile": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + organization_id=data["organization_id"], + connection_id=data["connection_id"], + connection_type=ProfileConnectionType(data["connection_type"]), + idp_id=data["idp_id"], + email=data["email"], + first_name=data["first_name"], + last_name=data["last_name"], + raw_attributes=data["raw_attributes"], + role=SlimRole.from_dict(cast(Dict[str, Any], _v)) + if (_v := data.get("role")) is not None + else None, + roles=[ + SlimRole.from_dict(cast(Dict[str, Any], item)) + for item in cast(list[Any], _v) + ] + if (_v := data.get("roles")) is not None + else None, + groups=data.get("groups"), + custom_attributes=data.get("custom_attributes"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing Profile: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + if self.organization_id is not None: + result["organization_id"] = self.organization_id + else: + result["organization_id"] = None + result["connection_id"] = self.connection_id + result["connection_type"] = self.connection_type + result["idp_id"] = self.idp_id + result["email"] = self.email + if self.first_name is not None: + result["first_name"] = self.first_name + else: + result["first_name"] = None + if self.last_name is not None: + result["last_name"] = self.last_name + else: + result["last_name"] = None + result["raw_attributes"] = self.raw_attributes + if self.role is not None: + result["role"] = self.role.to_dict() + else: + result["role"] = None + if self.roles is not None: + result["roles"] = [item.to_dict() for item in self.roles] + else: + result["roles"] = None + if self.groups is not None: + result["groups"] = self.groups + if self.custom_attributes is not None: + result["custom_attributes"] = self.custom_attributes + return result diff --git a/src/workos/sso/models/sso_authorize_url_response.py b/src/workos/sso/models/sso_authorize_url_response.py new file mode 100644 index 00000000..19a5b251 --- /dev/null +++ b/src/workos/sso/models/sso_authorize_url_response.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class SSOAuthorizeUrlResponse: + """SSO Authorize Url Response model.""" + + url: str + """An OAuth 2.0 authorization URL.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "SSOAuthorizeUrlResponse": + """Deserialize from a dictionary.""" + try: + return cls( + url=data["url"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing SSOAuthorizeUrlResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["url"] = self.url + return result diff --git a/src/workos/sso/models/sso_logout_authorize_request.py b/src/workos/sso/models/sso_logout_authorize_request.py new file mode 100644 index 00000000..673c17b5 --- /dev/null +++ b/src/workos/sso/models/sso_logout_authorize_request.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class SSOLogoutAuthorizeRequest: + """SSO Logout Authorize Request model.""" + + profile_id: str + """The unique ID of the profile to log out.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "SSOLogoutAuthorizeRequest": + """Deserialize from a dictionary.""" + try: + return cls( + profile_id=data["profile_id"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing SSOLogoutAuthorizeRequest: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["profile_id"] = self.profile_id + return result diff --git a/src/workos/sso/models/sso_logout_authorize_response.py b/src/workos/sso/models/sso_logout_authorize_response.py new file mode 100644 index 00000000..84fddeee --- /dev/null +++ b/src/workos/sso/models/sso_logout_authorize_response.py @@ -0,0 +1,37 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class SSOLogoutAuthorizeResponse: + """SSO Logout Authorize Response model.""" + + logout_url: str + """The URL to redirect the user to in order to log out ([Logout Redirect](https://workos.com/docs/reference/sso/logout) endpoint ready to use).""" + logout_token: str + """The logout token to be used in the [Logout Redirect](https://workos.com/docs/reference/sso/logout) endpoint.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "SSOLogoutAuthorizeResponse": + """Deserialize from a dictionary.""" + try: + return cls( + logout_url=data["logout_url"], + logout_token=data["logout_token"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing SSOLogoutAuthorizeResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["logout_url"] = self.logout_url + result["logout_token"] = self.logout_token + return result diff --git a/src/workos/sso/models/sso_provider.py b/src/workos/sso/models/sso_provider.py new file mode 100644 index 00000000..8ee73b03 --- /dev/null +++ b/src/workos/sso/models/sso_provider.py @@ -0,0 +1,32 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of sso provider values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class SSOProvider(str, Enum): + """Known values for SSOProvider.""" + + APPLE_OAUTH = "AppleOAuth" + GIT_HUB_OAUTH = "GitHubOAuth" + GOOGLE_OAUTH = "GoogleOAuth" + MICROSOFT_OAUTH = "MicrosoftOAuth" + + @classmethod + def _missing_(cls, value: object) -> Optional["SSOProvider"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +SSOProviderLiteral: TypeAlias = Literal[ + "AppleOAuth", "GitHubOAuth", "GoogleOAuth", "MicrosoftOAuth" +] diff --git a/src/workos/sso/models/sso_token_response.py b/src/workos/sso/models/sso_token_response.py new file mode 100644 index 00000000..84a8e83d --- /dev/null +++ b/src/workos/sso/models/sso_token_response.py @@ -0,0 +1,57 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + +from .profile import Profile +from .sso_token_response_oauth_token import SSOTokenResponseOAuthToken + + +@dataclass(slots=True) +class SSOTokenResponse: + """SSO Token Response model.""" + + token_type: Literal["Bearer"] + """The type of token issued.""" + access_token: str + """An access token that can be exchanged for a user profile. Access tokens are short-lived — see the `expires_in` field for the exact lifetime.""" + expires_in: int + """The lifetime of the access token in seconds.""" + profile: "Profile" + oauth_tokens: Optional["SSOTokenResponseOAuthToken"] = None + """OAuth tokens issued by the identity provider, if available.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "SSOTokenResponse": + """Deserialize from a dictionary.""" + try: + return cls( + token_type=data["token_type"], + access_token=data["access_token"], + expires_in=data["expires_in"], + profile=Profile.from_dict(cast(Dict[str, Any], data["profile"])), + oauth_tokens=SSOTokenResponseOAuthToken.from_dict( + cast(Dict[str, Any], _v) + ) + if (_v := data.get("oauth_tokens")) is not None + else None, + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing SSOTokenResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["token_type"] = self.token_type + result["access_token"] = self.access_token + result["expires_in"] = self.expires_in + result["profile"] = self.profile.to_dict() + if self.oauth_tokens is not None: + result["oauth_tokens"] = self.oauth_tokens.to_dict() + return result diff --git a/src/workos/sso/models/sso_token_response_oauth_token.py b/src/workos/sso/models/sso_token_response_oauth_token.py new file mode 100644 index 00000000..903dcb14 --- /dev/null +++ b/src/workos/sso/models/sso_token_response_oauth_token.py @@ -0,0 +1,49 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, List +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class SSOTokenResponseOAuthToken: + """OAuth tokens issued by the identity provider, if available.""" + + provider: str + """The OAuth provider used for authentication.""" + refresh_token: str + """The refresh token from the OAuth provider.""" + access_token: str + """The access token from the OAuth provider.""" + expires_at: int + """The timestamp at which the access token expires.""" + scopes: List[str] + """A list of OAuth scopes for which the access token is authorized.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "SSOTokenResponseOAuthToken": + """Deserialize from a dictionary.""" + try: + return cls( + provider=data["provider"], + refresh_token=data["refresh_token"], + access_token=data["access_token"], + expires_at=data["expires_at"], + scopes=data["scopes"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing SSOTokenResponseOAuthToken: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["provider"] = self.provider + result["refresh_token"] = self.refresh_token + result["access_token"] = self.access_token + result["expires_at"] = self.expires_at + result["scopes"] = self.scopes + return result diff --git a/src/workos/sso/models/token_query.py b/src/workos/sso/models/token_query.py new file mode 100644 index 00000000..421874a1 --- /dev/null +++ b/src/workos/sso/models/token_query.py @@ -0,0 +1,45 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Literal +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class TokenQuery: + """Token Query model.""" + + client_id: str + """The client ID of the WorkOS environment.""" + client_secret: str + """The client secret of the WorkOS environment.""" + code: str + """The authorization code received from the authorization callback.""" + grant_type: Literal["authorization_code"] + """The grant type for the token request.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "TokenQuery": + """Deserialize from a dictionary.""" + try: + return cls( + client_id=data["client_id"], + client_secret=data["client_secret"], + code=data["code"], + grant_type=data["grant_type"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing TokenQuery: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["client_id"] = self.client_id + result["client_secret"] = self.client_secret + result["code"] = self.code + result["grant_type"] = self.grant_type + return result diff --git a/src/workos/types/__init__.py b/src/workos/types/__init__.py new file mode 100644 index 00000000..33e9c7b7 --- /dev/null +++ b/src/workos/types/__init__.py @@ -0,0 +1,2 @@ +# This file is auto-generated by oagen. Do not edit. + diff --git a/src/workos/types/admin_portal/__init__.py b/src/workos/types/admin_portal/__init__.py new file mode 100644 index 00000000..d183d123 --- /dev/null +++ b/src/workos/types/admin_portal/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.admin_portal.models import * # noqa: F401,F403 diff --git a/src/workos/types/api_keys/__init__.py b/src/workos/types/api_keys/__init__.py new file mode 100644 index 00000000..d83f31ba --- /dev/null +++ b/src/workos/types/api_keys/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.api_keys.models import * # noqa: F401,F403 diff --git a/src/workos/types/application_client_secrets/__init__.py b/src/workos/types/application_client_secrets/__init__.py new file mode 100644 index 00000000..cdce13dd --- /dev/null +++ b/src/workos/types/application_client_secrets/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.application_client_secrets.models import * # noqa: F401,F403 diff --git a/src/workos/types/applications/__init__.py b/src/workos/types/applications/__init__.py new file mode 100644 index 00000000..9183ef05 --- /dev/null +++ b/src/workos/types/applications/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import * # noqa: F401,F403 diff --git a/src/workos/types/audit_logs/__init__.py b/src/workos/types/audit_logs/__init__.py new file mode 100644 index 00000000..7a55f4aa --- /dev/null +++ b/src/workos/types/audit_logs/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.audit_logs.models import * # noqa: F401,F403 diff --git a/src/workos/types/authorization/__init__.py b/src/workos/types/authorization/__init__.py new file mode 100644 index 00000000..599e9b5a --- /dev/null +++ b/src/workos/types/authorization/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.authorization.models import * # noqa: F401,F403 diff --git a/src/workos/types/connect/__init__.py b/src/workos/types/connect/__init__.py new file mode 100644 index 00000000..96cf742d --- /dev/null +++ b/src/workos/types/connect/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.workos_connect.models import * # noqa: F401,F403 diff --git a/src/workos/types/connections/__init__.py b/src/workos/types/connections/__init__.py new file mode 100644 index 00000000..d8bef48a --- /dev/null +++ b/src/workos/types/connections/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.connections.models import * # noqa: F401,F403 diff --git a/src/workos/types/directories/__init__.py b/src/workos/types/directories/__init__.py new file mode 100644 index 00000000..ffced000 --- /dev/null +++ b/src/workos/types/directories/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.directories.models import * # noqa: F401,F403 diff --git a/src/workos/types/directory_groups/__init__.py b/src/workos/types/directory_groups/__init__.py new file mode 100644 index 00000000..c257566b --- /dev/null +++ b/src/workos/types/directory_groups/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.directory_groups.models import * # noqa: F401,F403 diff --git a/src/workos/types/directory_sync/__init__.py b/src/workos/types/directory_sync/__init__.py new file mode 100644 index 00000000..ffced000 --- /dev/null +++ b/src/workos/types/directory_sync/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.directories.models import * # noqa: F401,F403 diff --git a/src/workos/types/directory_users/__init__.py b/src/workos/types/directory_users/__init__.py new file mode 100644 index 00000000..2a5bae9e --- /dev/null +++ b/src/workos/types/directory_users/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.directory_users.models import * # noqa: F401,F403 diff --git a/src/workos/types/events/__init__.py b/src/workos/types/events/__init__.py new file mode 100644 index 00000000..89fa42fe --- /dev/null +++ b/src/workos/types/events/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.events.models import * # noqa: F401,F403 diff --git a/src/workos/types/feature_flags/__init__.py b/src/workos/types/feature_flags/__init__.py new file mode 100644 index 00000000..63d36499 --- /dev/null +++ b/src/workos/types/feature_flags/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.feature_flags.models import * # noqa: F401,F403 +from workos.feature_flags.targets.models import * # noqa: F401,F403 diff --git a/src/workos/types/feature_flags/feature_flag.py b/src/workos/types/feature_flags/feature_flag.py new file mode 100644 index 00000000..79734b6e --- /dev/null +++ b/src/workos/types/feature_flags/feature_flag.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.feature_flags.models import FeatureFlag as FeatureFlag diff --git a/src/workos/types/feature_flags/feature_flag_owner.py b/src/workos/types/feature_flags/feature_flag_owner.py new file mode 100644 index 00000000..98c51846 --- /dev/null +++ b/src/workos/types/feature_flags/feature_flag_owner.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.feature_flags.models import FeatureFlagOwner as FeatureFlagOwner diff --git a/src/workos/types/feature_flags/flag.py b/src/workos/types/feature_flags/flag.py new file mode 100644 index 00000000..3a3b080e --- /dev/null +++ b/src/workos/types/feature_flags/flag.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.feature_flags.models import Flag as Flag diff --git a/src/workos/types/feature_flags/flag_owner.py b/src/workos/types/feature_flags/flag_owner.py new file mode 100644 index 00000000..241ad09d --- /dev/null +++ b/src/workos/types/feature_flags/flag_owner.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.feature_flags.models import FlagOwner as FlagOwner diff --git a/src/workos/types/feature_flags_targets/__init__.py b/src/workos/types/feature_flags_targets/__init__.py new file mode 100644 index 00000000..a0b603e2 --- /dev/null +++ b/src/workos/types/feature_flags_targets/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.feature_flags.targets.models import * # noqa: F401,F403 diff --git a/src/workos/types/fga/__init__.py b/src/workos/types/fga/__init__.py new file mode 100644 index 00000000..599e9b5a --- /dev/null +++ b/src/workos/types/fga/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.authorization.models import * # noqa: F401,F403 diff --git a/src/workos/types/mfa/__init__.py b/src/workos/types/mfa/__init__.py new file mode 100644 index 00000000..8c057ca6 --- /dev/null +++ b/src/workos/types/mfa/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.multi_factor_auth.models import * # noqa: F401,F403 diff --git a/src/workos/types/multi_factor_auth/__init__.py b/src/workos/types/multi_factor_auth/__init__.py new file mode 100644 index 00000000..cdc2ad05 --- /dev/null +++ b/src/workos/types/multi_factor_auth/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.multi_factor_auth.models import * # noqa: F401,F403 +from workos.multi_factor_auth.challenges.models import * # noqa: F401,F403 diff --git a/src/workos/types/multi_factor_auth/authentication_challenge.py b/src/workos/types/multi_factor_auth/authentication_challenge.py new file mode 100644 index 00000000..e48fb135 --- /dev/null +++ b/src/workos/types/multi_factor_auth/authentication_challenge.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.multi_factor_auth.challenges.models import ( + AuthenticationChallenge as AuthenticationChallenge, +) diff --git a/src/workos/types/multi_factor_auth/authentication_challenge_verify_response.py b/src/workos/types/multi_factor_auth/authentication_challenge_verify_response.py new file mode 100644 index 00000000..3563f3ef --- /dev/null +++ b/src/workos/types/multi_factor_auth/authentication_challenge_verify_response.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.multi_factor_auth.challenges.models import ( + AuthenticationChallengeVerifyResponse as AuthenticationChallengeVerifyResponse, +) diff --git a/src/workos/types/multi_factor_auth/authentication_challenges_verify_request.py b/src/workos/types/multi_factor_auth/authentication_challenges_verify_request.py new file mode 100644 index 00000000..4583e069 --- /dev/null +++ b/src/workos/types/multi_factor_auth/authentication_challenges_verify_request.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.multi_factor_auth.challenges.models import ( + AuthenticationChallengesVerifyRequest as AuthenticationChallengesVerifyRequest, +) diff --git a/src/workos/types/multi_factor_auth/authentication_factor.py b/src/workos/types/multi_factor_auth/authentication_factor.py new file mode 100644 index 00000000..7ddc28e6 --- /dev/null +++ b/src/workos/types/multi_factor_auth/authentication_factor.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.multi_factor_auth.models import AuthenticationFactor as AuthenticationFactor diff --git a/src/workos/types/multi_factor_auth/authentication_factor_enrolled.py b/src/workos/types/multi_factor_auth/authentication_factor_enrolled.py new file mode 100644 index 00000000..ba1adbf4 --- /dev/null +++ b/src/workos/types/multi_factor_auth/authentication_factor_enrolled.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.multi_factor_auth.models import ( + AuthenticationFactorEnrolled as AuthenticationFactorEnrolled, +) diff --git a/src/workos/types/multi_factor_auth/authentication_factor_enrolled_sms.py b/src/workos/types/multi_factor_auth/authentication_factor_enrolled_sms.py new file mode 100644 index 00000000..c711eadb --- /dev/null +++ b/src/workos/types/multi_factor_auth/authentication_factor_enrolled_sms.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.multi_factor_auth.models import ( + AuthenticationFactorEnrolledSms as AuthenticationFactorEnrolledSms, +) diff --git a/src/workos/types/multi_factor_auth/authentication_factor_enrolled_totp.py b/src/workos/types/multi_factor_auth/authentication_factor_enrolled_totp.py new file mode 100644 index 00000000..9b7f0e5f --- /dev/null +++ b/src/workos/types/multi_factor_auth/authentication_factor_enrolled_totp.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.multi_factor_auth.models import ( + AuthenticationFactorEnrolledTotp as AuthenticationFactorEnrolledTotp, +) diff --git a/src/workos/types/multi_factor_auth/authentication_factor_sms.py b/src/workos/types/multi_factor_auth/authentication_factor_sms.py new file mode 100644 index 00000000..febc0ddf --- /dev/null +++ b/src/workos/types/multi_factor_auth/authentication_factor_sms.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.multi_factor_auth.models import ( + AuthenticationFactorSms as AuthenticationFactorSms, +) diff --git a/src/workos/types/multi_factor_auth/authentication_factor_totp.py b/src/workos/types/multi_factor_auth/authentication_factor_totp.py new file mode 100644 index 00000000..51ed6dd7 --- /dev/null +++ b/src/workos/types/multi_factor_auth/authentication_factor_totp.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.multi_factor_auth.models import ( + AuthenticationFactorTotp as AuthenticationFactorTotp, +) diff --git a/src/workos/types/multi_factor_auth/authentication_factors_create_request.py b/src/workos/types/multi_factor_auth/authentication_factors_create_request.py new file mode 100644 index 00000000..14994ec9 --- /dev/null +++ b/src/workos/types/multi_factor_auth/authentication_factors_create_request.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.multi_factor_auth.models import ( + AuthenticationFactorsCreateRequest as AuthenticationFactorsCreateRequest, +) diff --git a/src/workos/types/multi_factor_auth/challenge_authentication_factor.py b/src/workos/types/multi_factor_auth/challenge_authentication_factor.py new file mode 100644 index 00000000..faaf16db --- /dev/null +++ b/src/workos/types/multi_factor_auth/challenge_authentication_factor.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.multi_factor_auth.models import ( + ChallengeAuthenticationFactor as ChallengeAuthenticationFactor, +) diff --git a/src/workos/types/multi_factor_auth_challenges/__init__.py b/src/workos/types/multi_factor_auth_challenges/__init__.py new file mode 100644 index 00000000..47ecdc38 --- /dev/null +++ b/src/workos/types/multi_factor_auth_challenges/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.multi_factor_auth.challenges.models import * # noqa: F401,F403 diff --git a/src/workos/types/organization_domains/__init__.py b/src/workos/types/organization_domains/__init__.py new file mode 100644 index 00000000..aa31be5f --- /dev/null +++ b/src/workos/types/organization_domains/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.organization_domains.models import * # noqa: F401,F403 diff --git a/src/workos/types/organizations/__init__.py b/src/workos/types/organizations/__init__.py new file mode 100644 index 00000000..cd384055 --- /dev/null +++ b/src/workos/types/organizations/__init__.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.organizations.models import * # noqa: F401,F403 +from workos.organizations.api_keys.models import * # noqa: F401,F403 +from workos.organizations.feature_flags.models import * # noqa: F401,F403 diff --git a/src/workos/types/organizations/api_key_with_value.py b/src/workos/types/organizations/api_key_with_value.py new file mode 100644 index 00000000..89123ea6 --- /dev/null +++ b/src/workos/types/organizations/api_key_with_value.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.organizations.api_keys.models import ApiKeyWithValue as ApiKeyWithValue diff --git a/src/workos/types/organizations/api_key_with_value_owner.py b/src/workos/types/organizations/api_key_with_value_owner.py new file mode 100644 index 00000000..b469d318 --- /dev/null +++ b/src/workos/types/organizations/api_key_with_value_owner.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.organizations.api_keys.models import ( + ApiKeyWithValueOwner as ApiKeyWithValueOwner, +) diff --git a/src/workos/types/organizations/audit_log_configuration.py b/src/workos/types/organizations/audit_log_configuration.py new file mode 100644 index 00000000..c912bea8 --- /dev/null +++ b/src/workos/types/organizations/audit_log_configuration.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.organizations.models import AuditLogConfiguration as AuditLogConfiguration diff --git a/src/workos/types/organizations/audit_log_configuration_log_stream.py b/src/workos/types/organizations/audit_log_configuration_log_stream.py new file mode 100644 index 00000000..59221d99 --- /dev/null +++ b/src/workos/types/organizations/audit_log_configuration_log_stream.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.organizations.models import ( + AuditLogConfigurationLogStream as AuditLogConfigurationLogStream, +) diff --git a/src/workos/types/organizations/audit_logs_retention_json.py b/src/workos/types/organizations/audit_logs_retention_json.py new file mode 100644 index 00000000..234bdae0 --- /dev/null +++ b/src/workos/types/organizations/audit_logs_retention_json.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.organizations.models import AuditLogsRetentionJson as AuditLogsRetentionJson diff --git a/src/workos/types/organizations/create_organization_api_key.py b/src/workos/types/organizations/create_organization_api_key.py new file mode 100644 index 00000000..157e2122 --- /dev/null +++ b/src/workos/types/organizations/create_organization_api_key.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.organizations.api_keys.models import ( + CreateOrganizationApiKey as CreateOrganizationApiKey, +) diff --git a/src/workos/types/organizations/organization.py b/src/workos/types/organizations/organization.py new file mode 100644 index 00000000..70ff31b6 --- /dev/null +++ b/src/workos/types/organizations/organization.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.organizations.models import Organization as Organization diff --git a/src/workos/types/organizations/organization_domain_data.py b/src/workos/types/organizations/organization_domain_data.py new file mode 100644 index 00000000..ea3e19c8 --- /dev/null +++ b/src/workos/types/organizations/organization_domain_data.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.organizations.models import OrganizationDomainData as OrganizationDomainData diff --git a/src/workos/types/organizations/organization_dto.py b/src/workos/types/organizations/organization_dto.py new file mode 100644 index 00000000..4707160a --- /dev/null +++ b/src/workos/types/organizations/organization_dto.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.organizations.models import OrganizationDto as OrganizationDto diff --git a/src/workos/types/organizations/update_audit_logs_retention.py b/src/workos/types/organizations/update_audit_logs_retention.py new file mode 100644 index 00000000..3667d6c5 --- /dev/null +++ b/src/workos/types/organizations/update_audit_logs_retention.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.organizations.models import ( + UpdateAuditLogsRetention as UpdateAuditLogsRetention, +) diff --git a/src/workos/types/organizations/update_organization.py b/src/workos/types/organizations/update_organization.py new file mode 100644 index 00000000..8bd8626c --- /dev/null +++ b/src/workos/types/organizations/update_organization.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.organizations.models import UpdateOrganization as UpdateOrganization diff --git a/src/workos/types/organizations_api_keys/__init__.py b/src/workos/types/organizations_api_keys/__init__.py new file mode 100644 index 00000000..114376c7 --- /dev/null +++ b/src/workos/types/organizations_api_keys/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.organizations.api_keys.models import * # noqa: F401,F403 diff --git a/src/workos/types/organizations_feature_flags/__init__.py b/src/workos/types/organizations_feature_flags/__init__.py new file mode 100644 index 00000000..b1e1a0a6 --- /dev/null +++ b/src/workos/types/organizations_feature_flags/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.organizations.feature_flags.models import * # noqa: F401,F403 diff --git a/src/workos/types/permissions/__init__.py b/src/workos/types/permissions/__init__.py new file mode 100644 index 00000000..6c9dc9d4 --- /dev/null +++ b/src/workos/types/permissions/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.permissions.models import * # noqa: F401,F403 diff --git a/src/workos/types/pipes/__init__.py b/src/workos/types/pipes/__init__.py new file mode 100644 index 00000000..dd3c36bd --- /dev/null +++ b/src/workos/types/pipes/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.pipes.models import * # noqa: F401,F403 diff --git a/src/workos/types/portal/__init__.py b/src/workos/types/portal/__init__.py new file mode 100644 index 00000000..d183d123 --- /dev/null +++ b/src/workos/types/portal/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.admin_portal.models import * # noqa: F401,F403 diff --git a/src/workos/types/radar/__init__.py b/src/workos/types/radar/__init__.py new file mode 100644 index 00000000..78661b8a --- /dev/null +++ b/src/workos/types/radar/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.radar.models import * # noqa: F401,F403 diff --git a/src/workos/types/sso/__init__.py b/src/workos/types/sso/__init__.py new file mode 100644 index 00000000..c16bc168 --- /dev/null +++ b/src/workos/types/sso/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.sso.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management/__init__.py b/src/workos/types/user_management/__init__.py new file mode 100644 index 00000000..28030de7 --- /dev/null +++ b/src/workos/types/user_management/__init__.py @@ -0,0 +1,13 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.authentication.models import * # noqa: F401,F403 +from workos.user_management.cors_origins.models import * # noqa: F401,F403 +from workos.user_management.data_providers.models import * # noqa: F401,F403 +from workos.user_management.invitations.models import * # noqa: F401,F403 +from workos.user_management.jwt_template.models import * # noqa: F401,F403 +from workos.user_management.magic_auth.models import * # noqa: F401,F403 +from workos.user_management.multi_factor_authentication.models import * # noqa: F401,F403 +from workos.user_management.organization_membership.models import * # noqa: F401,F403 +from workos.user_management.redirect_uris.models import * # noqa: F401,F403 +from workos.user_management.session_tokens.models import * # noqa: F401,F403 +from workos.user_management.users.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management/authenticate_response.py b/src/workos/types/user_management/authenticate_response.py new file mode 100644 index 00000000..0aa9eaff --- /dev/null +++ b/src/workos/types/user_management/authenticate_response.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.authentication.models import ( + AuthenticateResponse as AuthenticateResponse, +) diff --git a/src/workos/types/user_management/authenticate_response_impersonator.py b/src/workos/types/user_management/authenticate_response_impersonator.py new file mode 100644 index 00000000..d1f8cd6d --- /dev/null +++ b/src/workos/types/user_management/authenticate_response_impersonator.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.authentication.models import ( + AuthenticateResponseImpersonator as AuthenticateResponseImpersonator, +) diff --git a/src/workos/types/user_management/authenticate_response_oauth_token.py b/src/workos/types/user_management/authenticate_response_oauth_token.py new file mode 100644 index 00000000..efda7f93 --- /dev/null +++ b/src/workos/types/user_management/authenticate_response_oauth_token.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.authentication.models import ( + AuthenticateResponseOAuthToken as AuthenticateResponseOAuthToken, +) diff --git a/src/workos/types/user_management/authorization_code_session_authenticate_request.py b/src/workos/types/user_management/authorization_code_session_authenticate_request.py new file mode 100644 index 00000000..585beca5 --- /dev/null +++ b/src/workos/types/user_management/authorization_code_session_authenticate_request.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.authentication.models import ( + AuthorizationCodeSessionAuthenticateRequest as AuthorizationCodeSessionAuthenticateRequest, +) diff --git a/src/workos/types/user_management/connected_account.py b/src/workos/types/user_management/connected_account.py new file mode 100644 index 00000000..15350b69 --- /dev/null +++ b/src/workos/types/user_management/connected_account.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.data_providers.models import ( + ConnectedAccount as ConnectedAccount, +) diff --git a/src/workos/types/user_management/cors_origin_response.py b/src/workos/types/user_management/cors_origin_response.py new file mode 100644 index 00000000..b5b3b2cc --- /dev/null +++ b/src/workos/types/user_management/cors_origin_response.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.cors_origins.models import ( + CORSOriginResponse as CORSOriginResponse, +) diff --git a/src/workos/types/user_management/create_cors_origin.py b/src/workos/types/user_management/create_cors_origin.py new file mode 100644 index 00000000..9c30fedd --- /dev/null +++ b/src/workos/types/user_management/create_cors_origin.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.cors_origins.models import ( + CreateCORSOrigin as CreateCORSOrigin, +) diff --git a/src/workos/types/user_management/create_magic_code_and_return.py b/src/workos/types/user_management/create_magic_code_and_return.py new file mode 100644 index 00000000..e709342e --- /dev/null +++ b/src/workos/types/user_management/create_magic_code_and_return.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.magic_auth.models import ( + CreateMagicCodeAndReturn as CreateMagicCodeAndReturn, +) diff --git a/src/workos/types/user_management/create_password_reset.py b/src/workos/types/user_management/create_password_reset.py new file mode 100644 index 00000000..f3feae17 --- /dev/null +++ b/src/workos/types/user_management/create_password_reset.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.users.models import ( + CreatePasswordReset as CreatePasswordReset, +) diff --git a/src/workos/types/user_management/create_password_reset_token.py b/src/workos/types/user_management/create_password_reset_token.py new file mode 100644 index 00000000..2ba89b3f --- /dev/null +++ b/src/workos/types/user_management/create_password_reset_token.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.users.models import ( + CreatePasswordResetToken as CreatePasswordResetToken, +) diff --git a/src/workos/types/user_management/create_redirect_uri.py b/src/workos/types/user_management/create_redirect_uri.py new file mode 100644 index 00000000..a28ede70 --- /dev/null +++ b/src/workos/types/user_management/create_redirect_uri.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.redirect_uris.models import ( + CreateRedirectUri as CreateRedirectUri, +) diff --git a/src/workos/types/user_management/create_user.py b/src/workos/types/user_management/create_user.py new file mode 100644 index 00000000..d1976ab0 --- /dev/null +++ b/src/workos/types/user_management/create_user.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.users.models import CreateUser as CreateUser diff --git a/src/workos/types/user_management/create_user_invite_options.py b/src/workos/types/user_management/create_user_invite_options.py new file mode 100644 index 00000000..155b5691 --- /dev/null +++ b/src/workos/types/user_management/create_user_invite_options.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.invitations.models import ( + CreateUserInviteOptions as CreateUserInviteOptions, +) diff --git a/src/workos/types/user_management/create_user_organization_membership.py b/src/workos/types/user_management/create_user_organization_membership.py new file mode 100644 index 00000000..122e4259 --- /dev/null +++ b/src/workos/types/user_management/create_user_organization_membership.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.organization_membership.models import ( + CreateUserOrganizationMembership as CreateUserOrganizationMembership, +) diff --git a/src/workos/types/user_management/data_integrations_list_response.py b/src/workos/types/user_management/data_integrations_list_response.py new file mode 100644 index 00000000..763601c3 --- /dev/null +++ b/src/workos/types/user_management/data_integrations_list_response.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.data_providers.models import ( + DataIntegrationsListResponse as DataIntegrationsListResponse, +) diff --git a/src/workos/types/user_management/data_integrations_list_response_data.py b/src/workos/types/user_management/data_integrations_list_response_data.py new file mode 100644 index 00000000..b25dd3f2 --- /dev/null +++ b/src/workos/types/user_management/data_integrations_list_response_data.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.data_providers.models import ( + DataIntegrationsListResponseData as DataIntegrationsListResponseData, +) diff --git a/src/workos/types/user_management/data_integrations_list_response_data_connected_account.py b/src/workos/types/user_management/data_integrations_list_response_data_connected_account.py new file mode 100644 index 00000000..66167b4b --- /dev/null +++ b/src/workos/types/user_management/data_integrations_list_response_data_connected_account.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.data_providers.models import ( + DataIntegrationsListResponseDataConnectedAccount as DataIntegrationsListResponseDataConnectedAccount, +) diff --git a/src/workos/types/user_management/device_authorization_response.py b/src/workos/types/user_management/device_authorization_response.py new file mode 100644 index 00000000..81ec6786 --- /dev/null +++ b/src/workos/types/user_management/device_authorization_response.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.authentication.models import ( + DeviceAuthorizationResponse as DeviceAuthorizationResponse, +) diff --git a/src/workos/types/user_management/email_verification.py b/src/workos/types/user_management/email_verification.py new file mode 100644 index 00000000..fef458a1 --- /dev/null +++ b/src/workos/types/user_management/email_verification.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.users.models import EmailVerification as EmailVerification diff --git a/src/workos/types/user_management/enroll_user_authentication_factor.py b/src/workos/types/user_management/enroll_user_authentication_factor.py new file mode 100644 index 00000000..9a72c5f4 --- /dev/null +++ b/src/workos/types/user_management/enroll_user_authentication_factor.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.multi_factor_authentication.models import ( + EnrollUserAuthenticationFactor as EnrollUserAuthenticationFactor, +) diff --git a/src/workos/types/user_management/invitation.py b/src/workos/types/user_management/invitation.py new file mode 100644 index 00000000..a19b7412 --- /dev/null +++ b/src/workos/types/user_management/invitation.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.invitations.models import Invitation as Invitation diff --git a/src/workos/types/user_management/jwks_response.py b/src/workos/types/user_management/jwks_response.py new file mode 100644 index 00000000..ef78489e --- /dev/null +++ b/src/workos/types/user_management/jwks_response.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.session_tokens.models import JwksResponse as JwksResponse diff --git a/src/workos/types/user_management/jwks_response_keys.py b/src/workos/types/user_management/jwks_response_keys.py new file mode 100644 index 00000000..10ea04cf --- /dev/null +++ b/src/workos/types/user_management/jwks_response_keys.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.session_tokens.models import ( + JwksResponseKeys as JwksResponseKeys, +) diff --git a/src/workos/types/user_management/jwt_template_response.py b/src/workos/types/user_management/jwt_template_response.py new file mode 100644 index 00000000..1e071697 --- /dev/null +++ b/src/workos/types/user_management/jwt_template_response.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.jwt_template.models import ( + JWTTemplateResponse as JWTTemplateResponse, +) diff --git a/src/workos/types/user_management/magic_auth.py b/src/workos/types/user_management/magic_auth.py new file mode 100644 index 00000000..ed7a4d0c --- /dev/null +++ b/src/workos/types/user_management/magic_auth.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.magic_auth.models import MagicAuth as MagicAuth diff --git a/src/workos/types/user_management/organization_membership.py b/src/workos/types/user_management/organization_membership.py new file mode 100644 index 00000000..5a2af512 --- /dev/null +++ b/src/workos/types/user_management/organization_membership.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.organization_membership.models import ( + OrganizationMembership as OrganizationMembership, +) diff --git a/src/workos/types/user_management/password_reset.py b/src/workos/types/user_management/password_reset.py new file mode 100644 index 00000000..ba2ffa3d --- /dev/null +++ b/src/workos/types/user_management/password_reset.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.users.models import PasswordReset as PasswordReset diff --git a/src/workos/types/user_management/password_session_authenticate_request.py b/src/workos/types/user_management/password_session_authenticate_request.py new file mode 100644 index 00000000..b5809eef --- /dev/null +++ b/src/workos/types/user_management/password_session_authenticate_request.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.authentication.models import ( + PasswordSessionAuthenticateRequest as PasswordSessionAuthenticateRequest, +) diff --git a/src/workos/types/user_management/redirect_uri.py b/src/workos/types/user_management/redirect_uri.py new file mode 100644 index 00000000..cd0c6453 --- /dev/null +++ b/src/workos/types/user_management/redirect_uri.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.redirect_uris.models import RedirectUri as RedirectUri diff --git a/src/workos/types/user_management/refresh_token_session_authenticate_request.py b/src/workos/types/user_management/refresh_token_session_authenticate_request.py new file mode 100644 index 00000000..aba5c4ca --- /dev/null +++ b/src/workos/types/user_management/refresh_token_session_authenticate_request.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.authentication.models import ( + RefreshTokenSessionAuthenticateRequest as RefreshTokenSessionAuthenticateRequest, +) diff --git a/src/workos/types/user_management/resend_user_invite_options.py b/src/workos/types/user_management/resend_user_invite_options.py new file mode 100644 index 00000000..fb61163e --- /dev/null +++ b/src/workos/types/user_management/resend_user_invite_options.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.invitations.models import ( + ResendUserInviteOptions as ResendUserInviteOptions, +) diff --git a/src/workos/types/user_management/reset_password_response.py b/src/workos/types/user_management/reset_password_response.py new file mode 100644 index 00000000..91daa530 --- /dev/null +++ b/src/workos/types/user_management/reset_password_response.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.users.models import ( + ResetPasswordResponse as ResetPasswordResponse, +) diff --git a/src/workos/types/user_management/revoke_session.py b/src/workos/types/user_management/revoke_session.py new file mode 100644 index 00000000..f747f077 --- /dev/null +++ b/src/workos/types/user_management/revoke_session.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.authentication.models import RevokeSession as RevokeSession diff --git a/src/workos/types/user_management/send_verification_email_response.py b/src/workos/types/user_management/send_verification_email_response.py new file mode 100644 index 00000000..58a49100 --- /dev/null +++ b/src/workos/types/user_management/send_verification_email_response.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.users.models import ( + SendVerificationEmailResponse as SendVerificationEmailResponse, +) diff --git a/src/workos/types/user_management/sso_device_authorization_request.py b/src/workos/types/user_management/sso_device_authorization_request.py new file mode 100644 index 00000000..c0125940 --- /dev/null +++ b/src/workos/types/user_management/sso_device_authorization_request.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.authentication.models import ( + SSODeviceAuthorizationRequest as SSODeviceAuthorizationRequest, +) diff --git a/src/workos/types/user_management/update_jwt_template.py b/src/workos/types/user_management/update_jwt_template.py new file mode 100644 index 00000000..ce69d7ee --- /dev/null +++ b/src/workos/types/user_management/update_jwt_template.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.jwt_template.models import ( + UpdateJWTTemplate as UpdateJWTTemplate, +) diff --git a/src/workos/types/user_management/update_user.py b/src/workos/types/user_management/update_user.py new file mode 100644 index 00000000..df7312f0 --- /dev/null +++ b/src/workos/types/user_management/update_user.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.users.models import UpdateUser as UpdateUser diff --git a/src/workos/types/user_management/update_user_organization_membership.py b/src/workos/types/user_management/update_user_organization_membership.py new file mode 100644 index 00000000..56e23ae7 --- /dev/null +++ b/src/workos/types/user_management/update_user_organization_membership.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.organization_membership.models import ( + UpdateUserOrganizationMembership as UpdateUserOrganizationMembership, +) diff --git a/src/workos/types/user_management/urn_ietf_params_oauth_grant_type_device_code_session_authenticate_request.py b/src/workos/types/user_management/urn_ietf_params_oauth_grant_type_device_code_session_authenticate_request.py new file mode 100644 index 00000000..14ca100d --- /dev/null +++ b/src/workos/types/user_management/urn_ietf_params_oauth_grant_type_device_code_session_authenticate_request.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.authentication.models import ( + UrnIetfParamsOAuthGrantTypeDeviceCodeSessionAuthenticateRequest as UrnIetfParamsOAuthGrantTypeDeviceCodeSessionAuthenticateRequest, +) diff --git a/src/workos/types/user_management/urn_workos_oauth_grant_type_email_verification_code_session_authenticate_request.py b/src/workos/types/user_management/urn_workos_oauth_grant_type_email_verification_code_session_authenticate_request.py new file mode 100644 index 00000000..e1d27e99 --- /dev/null +++ b/src/workos/types/user_management/urn_workos_oauth_grant_type_email_verification_code_session_authenticate_request.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.authentication.models import ( + UrnWorkOSOAuthGrantTypeEmailVerificationCodeSessionAuthenticateRequest as UrnWorkOSOAuthGrantTypeEmailVerificationCodeSessionAuthenticateRequest, +) diff --git a/src/workos/types/user_management/urn_workos_oauth_grant_type_magic_auth_code_session_authenticate_request.py b/src/workos/types/user_management/urn_workos_oauth_grant_type_magic_auth_code_session_authenticate_request.py new file mode 100644 index 00000000..b1627305 --- /dev/null +++ b/src/workos/types/user_management/urn_workos_oauth_grant_type_magic_auth_code_session_authenticate_request.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.authentication.models import ( + UrnWorkOSOAuthGrantTypeMagicAuthCodeSessionAuthenticateRequest as UrnWorkOSOAuthGrantTypeMagicAuthCodeSessionAuthenticateRequest, +) diff --git a/src/workos/types/user_management/urn_workos_oauth_grant_type_mfa_totp_session_authenticate_request.py b/src/workos/types/user_management/urn_workos_oauth_grant_type_mfa_totp_session_authenticate_request.py new file mode 100644 index 00000000..51df7f3b --- /dev/null +++ b/src/workos/types/user_management/urn_workos_oauth_grant_type_mfa_totp_session_authenticate_request.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.authentication.models import ( + UrnWorkOSOAuthGrantTypeMFATotpSessionAuthenticateRequest as UrnWorkOSOAuthGrantTypeMFATotpSessionAuthenticateRequest, +) diff --git a/src/workos/types/user_management/urn_workos_oauth_grant_type_organization_selection_session_authenticate_request.py b/src/workos/types/user_management/urn_workos_oauth_grant_type_organization_selection_session_authenticate_request.py new file mode 100644 index 00000000..14e8d990 --- /dev/null +++ b/src/workos/types/user_management/urn_workos_oauth_grant_type_organization_selection_session_authenticate_request.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.authentication.models import ( + UrnWorkOSOAuthGrantTypeOrganizationSelectionSessionAuthenticateRequest as UrnWorkOSOAuthGrantTypeOrganizationSelectionSessionAuthenticateRequest, +) diff --git a/src/workos/types/user_management/user.py b/src/workos/types/user_management/user.py new file mode 100644 index 00000000..ac1834bd --- /dev/null +++ b/src/workos/types/user_management/user.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.authentication.models import User as User diff --git a/src/workos/types/user_management/user_authentication_factor_enroll_response.py b/src/workos/types/user_management/user_authentication_factor_enroll_response.py new file mode 100644 index 00000000..efe6016f --- /dev/null +++ b/src/workos/types/user_management/user_authentication_factor_enroll_response.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.multi_factor_authentication.models import ( + UserAuthenticationFactorEnrollResponse as UserAuthenticationFactorEnrollResponse, +) diff --git a/src/workos/types/user_management/user_identities_get_item.py b/src/workos/types/user_management/user_identities_get_item.py new file mode 100644 index 00000000..c12ce0b0 --- /dev/null +++ b/src/workos/types/user_management/user_identities_get_item.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.users.models import ( + UserIdentitiesGetItem as UserIdentitiesGetItem, +) diff --git a/src/workos/types/user_management/user_invite.py b/src/workos/types/user_management/user_invite.py new file mode 100644 index 00000000..68c0c6a7 --- /dev/null +++ b/src/workos/types/user_management/user_invite.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.invitations.models import UserInvite as UserInvite diff --git a/src/workos/types/user_management/user_organization_membership.py b/src/workos/types/user_management/user_organization_membership.py new file mode 100644 index 00000000..6231353e --- /dev/null +++ b/src/workos/types/user_management/user_organization_membership.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.organization_membership.models import ( + UserOrganizationMembership as UserOrganizationMembership, +) diff --git a/src/workos/types/user_management/user_sessions_impersonator.py b/src/workos/types/user_management/user_sessions_impersonator.py new file mode 100644 index 00000000..cb0efa1a --- /dev/null +++ b/src/workos/types/user_management/user_sessions_impersonator.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.users.models import ( + UserSessionsImpersonator as UserSessionsImpersonator, +) diff --git a/src/workos/types/user_management/user_sessions_list_item.py b/src/workos/types/user_management/user_sessions_list_item.py new file mode 100644 index 00000000..d2eae33c --- /dev/null +++ b/src/workos/types/user_management/user_sessions_list_item.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.users.models import ( + UserSessionsListItem as UserSessionsListItem, +) diff --git a/src/workos/types/user_management/verify_email_address.py b/src/workos/types/user_management/verify_email_address.py new file mode 100644 index 00000000..fa3bde3a --- /dev/null +++ b/src/workos/types/user_management/verify_email_address.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.users.models import VerifyEmailAddress as VerifyEmailAddress diff --git a/src/workos/types/user_management/verify_email_response.py b/src/workos/types/user_management/verify_email_response.py new file mode 100644 index 00000000..0068e16e --- /dev/null +++ b/src/workos/types/user_management/verify_email_response.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.users.models import ( + VerifyEmailResponse as VerifyEmailResponse, +) diff --git a/src/workos/types/user_management_authentication/__init__.py b/src/workos/types/user_management_authentication/__init__.py new file mode 100644 index 00000000..76f8d65a --- /dev/null +++ b/src/workos/types/user_management_authentication/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.authentication.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_cors_origins/__init__.py b/src/workos/types/user_management_cors_origins/__init__.py new file mode 100644 index 00000000..4a72e6d7 --- /dev/null +++ b/src/workos/types/user_management_cors_origins/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.cors_origins.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_data_providers/__init__.py b/src/workos/types/user_management_data_providers/__init__.py new file mode 100644 index 00000000..4bca66a1 --- /dev/null +++ b/src/workos/types/user_management_data_providers/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.data_providers.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_invitations/__init__.py b/src/workos/types/user_management_invitations/__init__.py new file mode 100644 index 00000000..1d575bad --- /dev/null +++ b/src/workos/types/user_management_invitations/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.invitations.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_jwt_template/__init__.py b/src/workos/types/user_management_jwt_template/__init__.py new file mode 100644 index 00000000..a596c8ea --- /dev/null +++ b/src/workos/types/user_management_jwt_template/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.jwt_template.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_magic_auth/__init__.py b/src/workos/types/user_management_magic_auth/__init__.py new file mode 100644 index 00000000..b9f6dcf7 --- /dev/null +++ b/src/workos/types/user_management_magic_auth/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.magic_auth.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_multi_factor_authentication/__init__.py b/src/workos/types/user_management_multi_factor_authentication/__init__.py new file mode 100644 index 00000000..c2c2ee13 --- /dev/null +++ b/src/workos/types/user_management_multi_factor_authentication/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.multi_factor_authentication.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_organization_membership/__init__.py b/src/workos/types/user_management_organization_membership/__init__.py new file mode 100644 index 00000000..d24e1869 --- /dev/null +++ b/src/workos/types/user_management_organization_membership/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.organization_membership.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_redirect_uris/__init__.py b/src/workos/types/user_management_redirect_uris/__init__.py new file mode 100644 index 00000000..f752307e --- /dev/null +++ b/src/workos/types/user_management_redirect_uris/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.redirect_uris.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_session_tokens/__init__.py b/src/workos/types/user_management_session_tokens/__init__.py new file mode 100644 index 00000000..438389b3 --- /dev/null +++ b/src/workos/types/user_management_session_tokens/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.session_tokens.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_users/__init__.py b/src/workos/types/user_management_users/__init__.py new file mode 100644 index 00000000..2d31c22c --- /dev/null +++ b/src/workos/types/user_management_users/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management_users.authorized_applications.models import * # noqa: F401,F403 +from workos.user_management_users.feature_flags.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_users/authorized_connect_application_list_data.py b/src/workos/types/user_management_users/authorized_connect_application_list_data.py new file mode 100644 index 00000000..ed1f6db2 --- /dev/null +++ b/src/workos/types/user_management_users/authorized_connect_application_list_data.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management_users.authorized_applications.models import ( + AuthorizedConnectApplicationListData as AuthorizedConnectApplicationListData, +) diff --git a/src/workos/types/user_management_users_authorized_applications/__init__.py b/src/workos/types/user_management_users_authorized_applications/__init__.py new file mode 100644 index 00000000..5d19dad3 --- /dev/null +++ b/src/workos/types/user_management_users_authorized_applications/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management_users.authorized_applications.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_users_feature_flags/__init__.py b/src/workos/types/user_management_users_feature_flags/__init__.py new file mode 100644 index 00000000..a2bd89c7 --- /dev/null +++ b/src/workos/types/user_management_users_feature_flags/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management_users.feature_flags.models import * # noqa: F401,F403 diff --git a/src/workos/types/webhooks/__init__.py b/src/workos/types/webhooks/__init__.py new file mode 100644 index 00000000..0b47a41b --- /dev/null +++ b/src/workos/types/webhooks/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.webhooks.models import * # noqa: F401,F403 diff --git a/src/workos/types/widgets/__init__.py b/src/workos/types/widgets/__init__.py new file mode 100644 index 00000000..0daafdc6 --- /dev/null +++ b/src/workos/types/widgets/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.widgets.models import * # noqa: F401,F403 diff --git a/src/workos/types/workos_connect/__init__.py b/src/workos/types/workos_connect/__init__.py new file mode 100644 index 00000000..96cf742d --- /dev/null +++ b/src/workos/types/workos_connect/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.workos_connect.models import * # noqa: F401,F403 diff --git a/src/workos/user_management/__init__.py b/src/workos/user_management/__init__.py new file mode 100644 index 00000000..33e9c7b7 --- /dev/null +++ b/src/workos/user_management/__init__.py @@ -0,0 +1,2 @@ +# This file is auto-generated by oagen. Do not edit. + diff --git a/src/workos/user_management/authentication/__init__.py b/src/workos/user_management/authentication/__init__.py new file mode 100644 index 00000000..9c4c44eb --- /dev/null +++ b/src/workos/user_management/authentication/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import UserManagementAuthentication, AsyncUserManagementAuthentication +from .models import * diff --git a/src/workos/user_management/authentication/_resource.py b/src/workos/user_management/authentication/_resource.py new file mode 100644 index 00000000..55c01041 --- /dev/null +++ b/src/workos/user_management/authentication/_resource.py @@ -0,0 +1,496 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union + +if TYPE_CHECKING: + from ..._client import AsyncWorkOSClient, WorkOSClient + +from .models import ( + AuthenticateResponse, + AuthorizationCodeSessionAuthenticateRequest, + DeviceAuthorizationResponse, + PasswordSessionAuthenticateRequest, + RefreshTokenSessionAuthenticateRequest, + UrnIetfParamsOAuthGrantTypeDeviceCodeSessionAuthenticateRequest, + UrnWorkOSOAuthGrantTypeEmailVerificationCodeSessionAuthenticateRequest, + UrnWorkOSOAuthGrantTypeMagicAuthCodeSessionAuthenticateRequest, + UrnWorkOSOAuthGrantTypeMFATotpSessionAuthenticateRequest, + UrnWorkOSOAuthGrantTypeOrganizationSelectionSessionAuthenticateRequest, +) +from .models import ( + UserManagementAuthenticationProvider, + UserManagementAuthenticationScreenHint, +) +from ..._types import RequestOptions + + +class UserManagementAuthentication: + """User Management Authentication API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def authenticate( + self, + *, + body: Union[ + AuthorizationCodeSessionAuthenticateRequest, + PasswordSessionAuthenticateRequest, + RefreshTokenSessionAuthenticateRequest, + UrnWorkOSOAuthGrantTypeMagicAuthCodeSessionAuthenticateRequest, + UrnWorkOSOAuthGrantTypeEmailVerificationCodeSessionAuthenticateRequest, + UrnWorkOSOAuthGrantTypeMFATotpSessionAuthenticateRequest, + UrnWorkOSOAuthGrantTypeOrganizationSelectionSessionAuthenticateRequest, + UrnIetfParamsOAuthGrantTypeDeviceCodeSessionAuthenticateRequest, + Dict[str, Any], + ], + request_options: Optional[RequestOptions] = None, + ) -> AuthenticateResponse: + """Authenticate + + Authenticate a user with a specified [authentication method](https://workos.com/docs/reference/authkit/authentication). + + Args: + body: The request body. Accepts: AuthorizationCodeSessionAuthenticateRequest, PasswordSessionAuthenticateRequest, RefreshTokenSessionAuthenticateRequest, UrnWorkOSOAuthGrantTypeMagicAuthCodeSessionAuthenticateRequest, UrnWorkOSOAuthGrantTypeEmailVerificationCodeSessionAuthenticateRequest, UrnWorkOSOAuthGrantTypeMFATotpSessionAuthenticateRequest, UrnWorkOSOAuthGrantTypeOrganizationSelectionSessionAuthenticateRequest, UrnIetfParamsOAuthGrantTypeDeviceCodeSessionAuthenticateRequest, or a plain dict. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthenticateResponse + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + RateLimitExceededException: If rate limited (429). + AuthenticationException: If the API key is invalid (401). + ServerException: If the server returns a 5xx error. + """ + _body: Dict[str, Any] = body if isinstance(body, dict) else body.to_dict() + return self._client.request( + method="post", + path="user_management/authenticate", + body=_body, + model=AuthenticateResponse, + request_options=request_options, + ) + + def authorize( + self, + *, + code_challenge_method: Optional[Literal["S256"]] = None, + code_challenge: Optional[str] = None, + domain_hint: Optional[str] = None, + connection_id: Optional[str] = None, + provider_query_params: Optional[Dict[str, str]] = None, + provider_scopes: Optional[List[str]] = None, + invitation_token: Optional[str] = None, + screen_hint: Optional[UserManagementAuthenticationScreenHint] = None, + login_hint: Optional[str] = None, + provider: Optional[UserManagementAuthenticationProvider] = None, + prompt: Optional[str] = None, + state: Optional[str] = None, + organization_id: Optional[str] = None, + response_type: Literal["code"], + redirect_uri: str, + client_id: str, + request_options: Optional[RequestOptions] = None, + ) -> str: + """Get an authorization URL + + Generates an OAuth 2.0 authorization URL to authenticate a user with AuthKit or SSO. + + Args: + code_challenge_method: The only valid PKCE code challenge method is `"S256"`. Required when specifying a `code_challenge`. + code_challenge: Code challenge derived from the code verifier used for the PKCE flow. + domain_hint: A domain hint for SSO connection lookup. + connection_id: The ID of an SSO connection to use for authentication. + provider_query_params: Key/value pairs of query parameters to pass to the OAuth provider. + provider_scopes: Additional OAuth scopes to request from the identity provider. + invitation_token: A token representing a user invitation to redeem during authentication. + screen_hint: Used to specify which screen to display when the provider is `authkit`. + login_hint: A hint to the authorization server about the login identifier the user might use. + provider: The OAuth provider to authenticate with (e.g., GoogleOAuth, MicrosoftOAuth, GitHubOAuth). + prompt: Controls the authentication flow behavior for the user. + state: An opaque value used to maintain state between the request and the callback. + organization_id: The ID of the organization to authenticate the user against. + response_type: The response type of the application. + redirect_uri: The callback URI where the authorization code will be sent after authentication. + client_id: The unique identifier of the WorkOS environment client. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + str + + Raises: + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "code_challenge_method": code_challenge_method, + "code_challenge": code_challenge, + "domain_hint": domain_hint, + "connection_id": connection_id, + "provider_query_params": provider_query_params, + "provider_scopes": provider_scopes, + "invitation_token": invitation_token, + "screen_hint": screen_hint.value if screen_hint else None, + "login_hint": login_hint, + "provider": provider.value if provider else None, + "prompt": prompt, + "state": state, + "organization_id": organization_id, + "response_type": response_type, + "redirect_uri": redirect_uri, + "client_id": client_id, + }.items() + if v is not None + } + return self._client.build_url("user_management/authorize", params) + + def device_authorization( + self, + *, + client_id: str, + request_options: Optional[RequestOptions] = None, + ) -> DeviceAuthorizationResponse: + """Get device authorization URL + + Initiates the CLI Auth flow by requesting a device code and verification URLs. This endpoint implements the OAuth 2.0 Device Authorization Flow ([RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628)) and is designed for command-line applications or other devices with limited input capabilities. + + Args: + client_id: The WorkOS client ID for your application. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + DeviceAuthorizationResponse + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "client_id": client_id, + } + return self._client.request( + method="post", + path="user_management/authorize/device", + body=body, + model=DeviceAuthorizationResponse, + request_options=request_options, + ) + + def logout( + self, + *, + session_id: str, + return_to: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> str: + """Logout + + Logout a user from the current [session](https://workos.com/docs/reference/authkit/session). + + Args: + session_id: The ID of the session to revoke. This can be extracted from the `sid` claim of the access token. + return_to: The URL to redirect the user to after session revocation. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + str + + Raises: + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "session_id": session_id, + "return_to": return_to, + }.items() + if v is not None + } + return self._client.build_url("user_management/sessions/logout", params) + + def revoke_session( + self, + *, + session_id: str, + return_to: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Revoke Session + + Revoke a [user session](https://workos.com/docs/reference/authkit/session). + + Args: + session_id: The ID of the session to revoke. This can be extracted from the `sid` claim of the access token. + return_to: The URL to redirect the user to after session revocation. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + BadRequestException: If the request is malformed (400). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "session_id": session_id, + "return_to": return_to, + }.items() + if v is not None + } + self._client.request( + method="post", + path="user_management/sessions/revoke", + body=body, + request_options=request_options, + ) + + +class AsyncUserManagementAuthentication: + """User Management Authentication API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def authenticate( + self, + *, + body: Union[ + AuthorizationCodeSessionAuthenticateRequest, + PasswordSessionAuthenticateRequest, + RefreshTokenSessionAuthenticateRequest, + UrnWorkOSOAuthGrantTypeMagicAuthCodeSessionAuthenticateRequest, + UrnWorkOSOAuthGrantTypeEmailVerificationCodeSessionAuthenticateRequest, + UrnWorkOSOAuthGrantTypeMFATotpSessionAuthenticateRequest, + UrnWorkOSOAuthGrantTypeOrganizationSelectionSessionAuthenticateRequest, + UrnIetfParamsOAuthGrantTypeDeviceCodeSessionAuthenticateRequest, + Dict[str, Any], + ], + request_options: Optional[RequestOptions] = None, + ) -> AuthenticateResponse: + """Authenticate + + Authenticate a user with a specified [authentication method](https://workos.com/docs/reference/authkit/authentication). + + Args: + body: The request body. Accepts: AuthorizationCodeSessionAuthenticateRequest, PasswordSessionAuthenticateRequest, RefreshTokenSessionAuthenticateRequest, UrnWorkOSOAuthGrantTypeMagicAuthCodeSessionAuthenticateRequest, UrnWorkOSOAuthGrantTypeEmailVerificationCodeSessionAuthenticateRequest, UrnWorkOSOAuthGrantTypeMFATotpSessionAuthenticateRequest, UrnWorkOSOAuthGrantTypeOrganizationSelectionSessionAuthenticateRequest, UrnIetfParamsOAuthGrantTypeDeviceCodeSessionAuthenticateRequest, or a plain dict. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AuthenticateResponse + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + RateLimitExceededException: If rate limited (429). + AuthenticationException: If the API key is invalid (401). + ServerException: If the server returns a 5xx error. + """ + _body: Dict[str, Any] = body if isinstance(body, dict) else body.to_dict() + return await self._client.request( + method="post", + path="user_management/authenticate", + body=_body, + model=AuthenticateResponse, + request_options=request_options, + ) + + async def authorize( + self, + *, + code_challenge_method: Optional[Literal["S256"]] = None, + code_challenge: Optional[str] = None, + domain_hint: Optional[str] = None, + connection_id: Optional[str] = None, + provider_query_params: Optional[Dict[str, str]] = None, + provider_scopes: Optional[List[str]] = None, + invitation_token: Optional[str] = None, + screen_hint: Optional[UserManagementAuthenticationScreenHint] = None, + login_hint: Optional[str] = None, + provider: Optional[UserManagementAuthenticationProvider] = None, + prompt: Optional[str] = None, + state: Optional[str] = None, + organization_id: Optional[str] = None, + response_type: Literal["code"], + redirect_uri: str, + client_id: str, + request_options: Optional[RequestOptions] = None, + ) -> str: + """Get an authorization URL + + Generates an OAuth 2.0 authorization URL to authenticate a user with AuthKit or SSO. + + Args: + code_challenge_method: The only valid PKCE code challenge method is `"S256"`. Required when specifying a `code_challenge`. + code_challenge: Code challenge derived from the code verifier used for the PKCE flow. + domain_hint: A domain hint for SSO connection lookup. + connection_id: The ID of an SSO connection to use for authentication. + provider_query_params: Key/value pairs of query parameters to pass to the OAuth provider. + provider_scopes: Additional OAuth scopes to request from the identity provider. + invitation_token: A token representing a user invitation to redeem during authentication. + screen_hint: Used to specify which screen to display when the provider is `authkit`. + login_hint: A hint to the authorization server about the login identifier the user might use. + provider: The OAuth provider to authenticate with (e.g., GoogleOAuth, MicrosoftOAuth, GitHubOAuth). + prompt: Controls the authentication flow behavior for the user. + state: An opaque value used to maintain state between the request and the callback. + organization_id: The ID of the organization to authenticate the user against. + response_type: The response type of the application. + redirect_uri: The callback URI where the authorization code will be sent after authentication. + client_id: The unique identifier of the WorkOS environment client. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + str + + Raises: + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "code_challenge_method": code_challenge_method, + "code_challenge": code_challenge, + "domain_hint": domain_hint, + "connection_id": connection_id, + "provider_query_params": provider_query_params, + "provider_scopes": provider_scopes, + "invitation_token": invitation_token, + "screen_hint": screen_hint.value if screen_hint else None, + "login_hint": login_hint, + "provider": provider.value if provider else None, + "prompt": prompt, + "state": state, + "organization_id": organization_id, + "response_type": response_type, + "redirect_uri": redirect_uri, + "client_id": client_id, + }.items() + if v is not None + } + return self._client.build_url("user_management/authorize", params) + + async def device_authorization( + self, + *, + client_id: str, + request_options: Optional[RequestOptions] = None, + ) -> DeviceAuthorizationResponse: + """Get device authorization URL + + Initiates the CLI Auth flow by requesting a device code and verification URLs. This endpoint implements the OAuth 2.0 Device Authorization Flow ([RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628)) and is designed for command-line applications or other devices with limited input capabilities. + + Args: + client_id: The WorkOS client ID for your application. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + DeviceAuthorizationResponse + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "client_id": client_id, + } + return await self._client.request( + method="post", + path="user_management/authorize/device", + body=body, + model=DeviceAuthorizationResponse, + request_options=request_options, + ) + + async def logout( + self, + *, + session_id: str, + return_to: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> str: + """Logout + + Logout a user from the current [session](https://workos.com/docs/reference/authkit/session). + + Args: + session_id: The ID of the session to revoke. This can be extracted from the `sid` claim of the access token. + return_to: The URL to redirect the user to after session revocation. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + str + + Raises: + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "session_id": session_id, + "return_to": return_to, + }.items() + if v is not None + } + return self._client.build_url("user_management/sessions/logout", params) + + async def revoke_session( + self, + *, + session_id: str, + return_to: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Revoke Session + + Revoke a [user session](https://workos.com/docs/reference/authkit/session). + + Args: + session_id: The ID of the session to revoke. This can be extracted from the `sid` claim of the access token. + return_to: The URL to redirect the user to after session revocation. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + BadRequestException: If the request is malformed (400). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "session_id": session_id, + "return_to": return_to, + }.items() + if v is not None + } + await self._client.request( + method="post", + path="user_management/sessions/revoke", + body=body, + request_options=request_options, + ) diff --git a/src/workos/user_management/authentication/models/__init__.py b/src/workos/user_management/authentication/models/__init__.py new file mode 100644 index 00000000..b09cdf3c --- /dev/null +++ b/src/workos/user_management/authentication/models/__init__.py @@ -0,0 +1,48 @@ +# This file is auto-generated by oagen. Do not edit. + +from .authenticate_response import AuthenticateResponse as AuthenticateResponse +from .authenticate_response_impersonator import ( + AuthenticateResponseImpersonator as AuthenticateResponseImpersonator, +) +from .authenticate_response_oauth_token import ( + AuthenticateResponseOAuthToken as AuthenticateResponseOAuthToken, +) +from .authorization_code_session_authenticate_request import ( + AuthorizationCodeSessionAuthenticateRequest as AuthorizationCodeSessionAuthenticateRequest, +) +from .device_authorization_response import ( + DeviceAuthorizationResponse as DeviceAuthorizationResponse, +) +from .password_session_authenticate_request import ( + PasswordSessionAuthenticateRequest as PasswordSessionAuthenticateRequest, +) +from .refresh_token_session_authenticate_request import ( + RefreshTokenSessionAuthenticateRequest as RefreshTokenSessionAuthenticateRequest, +) +from .revoke_session import RevokeSession as RevokeSession +from .sso_device_authorization_request import ( + SSODeviceAuthorizationRequest as SSODeviceAuthorizationRequest, +) +from .screen_hint_type import ScreenHintType as ScreenHintType +from .urn_ietf_params_oauth_grant_type_device_code_session_authenticate_request import ( + UrnIetfParamsOAuthGrantTypeDeviceCodeSessionAuthenticateRequest as UrnIetfParamsOAuthGrantTypeDeviceCodeSessionAuthenticateRequest, +) +from .urn_workos_oauth_grant_type_email_verification_code_session_authenticate_request import ( + UrnWorkOSOAuthGrantTypeEmailVerificationCodeSessionAuthenticateRequest as UrnWorkOSOAuthGrantTypeEmailVerificationCodeSessionAuthenticateRequest, +) +from .urn_workos_oauth_grant_type_magic_auth_code_session_authenticate_request import ( + UrnWorkOSOAuthGrantTypeMagicAuthCodeSessionAuthenticateRequest as UrnWorkOSOAuthGrantTypeMagicAuthCodeSessionAuthenticateRequest, +) +from .urn_workos_oauth_grant_type_mfa_totp_session_authenticate_request import ( + UrnWorkOSOAuthGrantTypeMFATotpSessionAuthenticateRequest as UrnWorkOSOAuthGrantTypeMFATotpSessionAuthenticateRequest, +) +from .urn_workos_oauth_grant_type_organization_selection_session_authenticate_request import ( + UrnWorkOSOAuthGrantTypeOrganizationSelectionSessionAuthenticateRequest as UrnWorkOSOAuthGrantTypeOrganizationSelectionSessionAuthenticateRequest, +) +from .user import User as User +from .user_management_authentication_provider import ( + UserManagementAuthenticationProvider as UserManagementAuthenticationProvider, +) +from .user_management_authentication_screen_hint import ( + UserManagementAuthenticationScreenHint as UserManagementAuthenticationScreenHint, +) diff --git a/src/workos/user_management/authentication/models/authenticate_response.py b/src/workos/user_management/authentication/models/authenticate_response.py new file mode 100644 index 00000000..c813ba85 --- /dev/null +++ b/src/workos/user_management/authentication/models/authenticate_response.py @@ -0,0 +1,82 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + +from .authenticate_response_impersonator import AuthenticateResponseImpersonator +from .authenticate_response_oauth_token import AuthenticateResponseOAuthToken +from .user import User +from workos.common.models import AuthenticateResponseAuthenticationMethod + + +@dataclass(slots=True) +class AuthenticateResponse: + """Authenticate Response model.""" + + user: "User" + """The corresponding [user](https://workos.com/docs/reference/authkit/user) object.""" + access_token: str + """A JWT containing information about the current session.""" + refresh_token: str + """[Exchange this token](https://workos.com/docs/reference/authkit/authentication/refresh-token) for a new access token.""" + organization_id: Optional[str] = None + """The ID of the organization the user selected to sign in to.""" + authkit_authorization_code: Optional[str] = None + """An authorization code that can be exchanged for tokens by a different application.""" + authentication_method: Optional["AuthenticateResponseAuthenticationMethod"] = None + """The authentication method used to initiate the session.""" + impersonator: Optional["AuthenticateResponseImpersonator"] = None + """Information about the impersonator if this session was created via impersonation.""" + oauth_tokens: Optional["AuthenticateResponseOAuthToken"] = None + """The OAuth tokens from the identity provider, if applicable.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuthenticateResponse": + """Deserialize from a dictionary.""" + try: + return cls( + user=User.from_dict(cast(Dict[str, Any], data["user"])), + access_token=data["access_token"], + refresh_token=data["refresh_token"], + organization_id=data.get("organization_id"), + authkit_authorization_code=data.get("authkit_authorization_code"), + authentication_method=AuthenticateResponseAuthenticationMethod(_v) + if (_v := data.get("authentication_method")) is not None + else None, + impersonator=AuthenticateResponseImpersonator.from_dict( + cast(Dict[str, Any], _v) + ) + if (_v := data.get("impersonator")) is not None + else None, + oauth_tokens=AuthenticateResponseOAuthToken.from_dict( + cast(Dict[str, Any], _v) + ) + if (_v := data.get("oauth_tokens")) is not None + else None, + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuthenticateResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["user"] = self.user.to_dict() + result["access_token"] = self.access_token + result["refresh_token"] = self.refresh_token + if self.organization_id is not None: + result["organization_id"] = self.organization_id + if self.authkit_authorization_code is not None: + result["authkit_authorization_code"] = self.authkit_authorization_code + if self.authentication_method is not None: + result["authentication_method"] = self.authentication_method + if self.impersonator is not None: + result["impersonator"] = self.impersonator.to_dict() + if self.oauth_tokens is not None: + result["oauth_tokens"] = self.oauth_tokens.to_dict() + return result diff --git a/src/workos/user_management/authentication/models/authenticate_response_impersonator.py b/src/workos/user_management/authentication/models/authenticate_response_impersonator.py new file mode 100644 index 00000000..c0b5c504 --- /dev/null +++ b/src/workos/user_management/authentication/models/authenticate_response_impersonator.py @@ -0,0 +1,40 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AuthenticateResponseImpersonator: + """Information about the impersonator if this session was created via impersonation.""" + + email: str + """The email address of the WorkOS Dashboard user who is impersonating the user.""" + reason: Optional[str] + """The justification the impersonator gave for impersonating the user.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuthenticateResponseImpersonator": + """Deserialize from a dictionary.""" + try: + return cls( + email=data["email"], + reason=data["reason"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuthenticateResponseImpersonator: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["email"] = self.email + if self.reason is not None: + result["reason"] = self.reason + else: + result["reason"] = None + return result diff --git a/src/workos/user_management/authentication/models/authenticate_response_oauth_token.py b/src/workos/user_management/authentication/models/authenticate_response_oauth_token.py new file mode 100644 index 00000000..58748773 --- /dev/null +++ b/src/workos/user_management/authentication/models/authenticate_response_oauth_token.py @@ -0,0 +1,49 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, List +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AuthenticateResponseOAuthToken: + """The OAuth tokens from the identity provider, if applicable.""" + + provider: str + """The OAuth provider used for authentication.""" + refresh_token: str + """The refresh token from the OAuth provider.""" + access_token: str + """The access token from the OAuth provider.""" + expires_at: int + """The timestamp at which the access token expires.""" + scopes: List[str] + """A list of OAuth scopes for which the access token is authorized.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuthenticateResponseOAuthToken": + """Deserialize from a dictionary.""" + try: + return cls( + provider=data["provider"], + refresh_token=data["refresh_token"], + access_token=data["access_token"], + expires_at=data["expires_at"], + scopes=data["scopes"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuthenticateResponseOAuthToken: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["provider"] = self.provider + result["refresh_token"] = self.refresh_token + result["access_token"] = self.access_token + result["expires_at"] = self.expires_at + result["scopes"] = self.scopes + return result diff --git a/src/workos/user_management/authentication/models/authorization_code_session_authenticate_request.py b/src/workos/user_management/authentication/models/authorization_code_session_authenticate_request.py new file mode 100644 index 00000000..a833a348 --- /dev/null +++ b/src/workos/user_management/authentication/models/authorization_code_session_authenticate_request.py @@ -0,0 +1,61 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class AuthorizationCodeSessionAuthenticateRequest: + """Authorization Code Session Authenticate Request model.""" + + client_id: str + """The client ID of the application.""" + client_secret: str + """The client secret of the application.""" + grant_type: Literal["authorization_code"] + code: str + """The authorization code received from the redirect.""" + ip_address: Optional[str] = None + """The IP address of the user's request.""" + device_id: Optional[str] = None + """A unique identifier for the device.""" + user_agent: Optional[str] = None + """The user agent string from the user's browser.""" + + @classmethod + def from_dict( + cls, data: Dict[str, Any] + ) -> "AuthorizationCodeSessionAuthenticateRequest": + """Deserialize from a dictionary.""" + try: + return cls( + client_id=data["client_id"], + client_secret=data["client_secret"], + grant_type=data["grant_type"], + code=data["code"], + ip_address=data.get("ip_address"), + device_id=data.get("device_id"), + user_agent=data.get("user_agent"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuthorizationCodeSessionAuthenticateRequest: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["client_id"] = self.client_id + result["client_secret"] = self.client_secret + result["grant_type"] = self.grant_type + result["code"] = self.code + if self.ip_address is not None: + result["ip_address"] = self.ip_address + if self.device_id is not None: + result["device_id"] = self.device_id + if self.user_agent is not None: + result["user_agent"] = self.user_agent + return result diff --git a/src/workos/user_management/authentication/models/device_authorization_response.py b/src/workos/user_management/authentication/models/device_authorization_response.py new file mode 100644 index 00000000..d46e1b20 --- /dev/null +++ b/src/workos/user_management/authentication/models/device_authorization_response.py @@ -0,0 +1,55 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class DeviceAuthorizationResponse: + """Device Authorization Response model.""" + + device_code: str + """The device verification code.""" + user_code: str + """The end-user verification code.""" + verification_uri: str + """The end-user verification URI.""" + expires_in: float + """Lifetime in seconds of the codes.""" + verification_uri_complete: Optional[str] = None + """Verification URI that includes the user code.""" + interval: Optional[float] = None + """Minimum polling interval in seconds.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "DeviceAuthorizationResponse": + """Deserialize from a dictionary.""" + try: + return cls( + device_code=data["device_code"], + user_code=data["user_code"], + verification_uri=data["verification_uri"], + expires_in=data["expires_in"], + verification_uri_complete=data.get("verification_uri_complete"), + interval=data.get("interval"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing DeviceAuthorizationResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["device_code"] = self.device_code + result["user_code"] = self.user_code + result["verification_uri"] = self.verification_uri + result["expires_in"] = self.expires_in + if self.verification_uri_complete is not None: + result["verification_uri_complete"] = self.verification_uri_complete + if self.interval is not None: + result["interval"] = self.interval + return result diff --git a/src/workos/user_management/authentication/models/password_session_authenticate_request.py b/src/workos/user_management/authentication/models/password_session_authenticate_request.py new file mode 100644 index 00000000..fbf636c9 --- /dev/null +++ b/src/workos/user_management/authentication/models/password_session_authenticate_request.py @@ -0,0 +1,68 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class PasswordSessionAuthenticateRequest: + """Password Session Authenticate Request model.""" + + client_id: str + """The client ID of the application.""" + client_secret: str + """The client secret of the application.""" + grant_type: Literal["password"] + email: str + """The user's email address.""" + password: str + """The user's password.""" + invitation_token: Optional[str] = None + """An invitation token to accept during authentication.""" + ip_address: Optional[str] = None + """The IP address of the user's request.""" + device_id: Optional[str] = None + """A unique identifier for the device.""" + user_agent: Optional[str] = None + """The user agent string from the user's browser.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "PasswordSessionAuthenticateRequest": + """Deserialize from a dictionary.""" + try: + return cls( + client_id=data["client_id"], + client_secret=data["client_secret"], + grant_type=data["grant_type"], + email=data["email"], + password=data["password"], + invitation_token=data.get("invitation_token"), + ip_address=data.get("ip_address"), + device_id=data.get("device_id"), + user_agent=data.get("user_agent"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing PasswordSessionAuthenticateRequest: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["client_id"] = self.client_id + result["client_secret"] = self.client_secret + result["grant_type"] = self.grant_type + result["email"] = self.email + result["password"] = self.password + if self.invitation_token is not None: + result["invitation_token"] = self.invitation_token + if self.ip_address is not None: + result["ip_address"] = self.ip_address + if self.device_id is not None: + result["device_id"] = self.device_id + if self.user_agent is not None: + result["user_agent"] = self.user_agent + return result diff --git a/src/workos/user_management/authentication/models/refresh_token_session_authenticate_request.py b/src/workos/user_management/authentication/models/refresh_token_session_authenticate_request.py new file mode 100644 index 00000000..f408e28c --- /dev/null +++ b/src/workos/user_management/authentication/models/refresh_token_session_authenticate_request.py @@ -0,0 +1,66 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class RefreshTokenSessionAuthenticateRequest: + """Refresh Token Session Authenticate Request model.""" + + client_id: str + """The client ID of the application.""" + client_secret: str + """The client secret of the application.""" + grant_type: Literal["refresh_token"] + refresh_token: str + """The refresh token to exchange for new tokens.""" + organization_id: Optional[str] = None + """The ID of the organization to scope the session to.""" + ip_address: Optional[str] = None + """The IP address of the user's request.""" + device_id: Optional[str] = None + """A unique identifier for the device.""" + user_agent: Optional[str] = None + """The user agent string from the user's browser.""" + + @classmethod + def from_dict( + cls, data: Dict[str, Any] + ) -> "RefreshTokenSessionAuthenticateRequest": + """Deserialize from a dictionary.""" + try: + return cls( + client_id=data["client_id"], + client_secret=data["client_secret"], + grant_type=data["grant_type"], + refresh_token=data["refresh_token"], + organization_id=data.get("organization_id"), + ip_address=data.get("ip_address"), + device_id=data.get("device_id"), + user_agent=data.get("user_agent"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing RefreshTokenSessionAuthenticateRequest: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["client_id"] = self.client_id + result["client_secret"] = self.client_secret + result["grant_type"] = self.grant_type + result["refresh_token"] = self.refresh_token + if self.organization_id is not None: + result["organization_id"] = self.organization_id + if self.ip_address is not None: + result["ip_address"] = self.ip_address + if self.device_id is not None: + result["device_id"] = self.device_id + if self.user_agent is not None: + result["user_agent"] = self.user_agent + return result diff --git a/src/workos/user_management/authentication/models/revoke_session.py b/src/workos/user_management/authentication/models/revoke_session.py new file mode 100644 index 00000000..a7501859 --- /dev/null +++ b/src/workos/user_management/authentication/models/revoke_session.py @@ -0,0 +1,38 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class RevokeSession: + """Revoke Session model.""" + + session_id: str + """The ID of the session to revoke. This can be extracted from the `sid` claim of the access token.""" + return_to: Optional[str] = None + """The URL to redirect the user to after session revocation.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "RevokeSession": + """Deserialize from a dictionary.""" + try: + return cls( + session_id=data["session_id"], + return_to=data.get("return_to"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing RevokeSession: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["session_id"] = self.session_id + if self.return_to is not None: + result["return_to"] = self.return_to + return result diff --git a/src/workos/user_management/authentication/models/screen_hint_type.py b/src/workos/user_management/authentication/models/screen_hint_type.py new file mode 100644 index 00000000..34505ec7 --- /dev/null +++ b/src/workos/user_management/authentication/models/screen_hint_type.py @@ -0,0 +1,8 @@ +# This file is auto-generated by oagen. Do not edit. + +from .user_management_authentication_screen_hint import ( + UserManagementAuthenticationScreenHint, +) + +ScreenHintType = UserManagementAuthenticationScreenHint +__all__ = ["ScreenHintType"] diff --git a/src/workos/user_management/authentication/models/sso_device_authorization_request.py b/src/workos/user_management/authentication/models/sso_device_authorization_request.py new file mode 100644 index 00000000..44f89402 --- /dev/null +++ b/src/workos/user_management/authentication/models/sso_device_authorization_request.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class SSODeviceAuthorizationRequest: + """SSO Device Authorization Request model.""" + + client_id: str + """The WorkOS client ID for your application.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "SSODeviceAuthorizationRequest": + """Deserialize from a dictionary.""" + try: + return cls( + client_id=data["client_id"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing SSODeviceAuthorizationRequest: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["client_id"] = self.client_id + return result diff --git a/src/workos/user_management/authentication/models/urn_ietf_params_oauth_grant_type_device_code_session_authenticate_request.py b/src/workos/user_management/authentication/models/urn_ietf_params_oauth_grant_type_device_code_session_authenticate_request.py new file mode 100644 index 00000000..c0d9254c --- /dev/null +++ b/src/workos/user_management/authentication/models/urn_ietf_params_oauth_grant_type_device_code_session_authenticate_request.py @@ -0,0 +1,57 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class UrnIetfParamsOAuthGrantTypeDeviceCodeSessionAuthenticateRequest: + """Urn Ietf Params O Auth Grant Type Device Code Session Authenticate Request model.""" + + client_id: str + """The client ID of the application.""" + grant_type: Literal["urn:ietf:params:oauth:grant-type:device_code"] + device_code: str + """The device verification code.""" + ip_address: Optional[str] = None + """The IP address of the user's request.""" + device_id: Optional[str] = None + """A unique identifier for the device.""" + user_agent: Optional[str] = None + """The user agent string from the user's browser.""" + + @classmethod + def from_dict( + cls, data: Dict[str, Any] + ) -> "UrnIetfParamsOAuthGrantTypeDeviceCodeSessionAuthenticateRequest": + """Deserialize from a dictionary.""" + try: + return cls( + client_id=data["client_id"], + grant_type=data["grant_type"], + device_code=data["device_code"], + ip_address=data.get("ip_address"), + device_id=data.get("device_id"), + user_agent=data.get("user_agent"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UrnIetfParamsOAuthGrantTypeDeviceCodeSessionAuthenticateRequest: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["client_id"] = self.client_id + result["grant_type"] = self.grant_type + result["device_code"] = self.device_code + if self.ip_address is not None: + result["ip_address"] = self.ip_address + if self.device_id is not None: + result["device_id"] = self.device_id + if self.user_agent is not None: + result["user_agent"] = self.user_agent + return result diff --git a/src/workos/user_management/authentication/models/urn_workos_oauth_grant_type_email_verification_code_session_authenticate_request.py b/src/workos/user_management/authentication/models/urn_workos_oauth_grant_type_email_verification_code_session_authenticate_request.py new file mode 100644 index 00000000..ad43e7c6 --- /dev/null +++ b/src/workos/user_management/authentication/models/urn_workos_oauth_grant_type_email_verification_code_session_authenticate_request.py @@ -0,0 +1,65 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class UrnWorkOSOAuthGrantTypeEmailVerificationCodeSessionAuthenticateRequest: + """Urn Work OSO Auth Grant Type Email Verification Code Session Authenticate Request model.""" + + client_id: str + """The client ID of the application.""" + client_secret: str + """The client secret of the application.""" + grant_type: Literal["urn:workos:oauth:grant-type:email-verification:code"] + code: str + """The email verification code.""" + pending_authentication_token: str + """The pending authentication token from a previous authentication attempt.""" + ip_address: Optional[str] = None + """The IP address of the user's request.""" + device_id: Optional[str] = None + """A unique identifier for the device.""" + user_agent: Optional[str] = None + """The user agent string from the user's browser.""" + + @classmethod + def from_dict( + cls, data: Dict[str, Any] + ) -> "UrnWorkOSOAuthGrantTypeEmailVerificationCodeSessionAuthenticateRequest": + """Deserialize from a dictionary.""" + try: + return cls( + client_id=data["client_id"], + client_secret=data["client_secret"], + grant_type=data["grant_type"], + code=data["code"], + pending_authentication_token=data["pending_authentication_token"], + ip_address=data.get("ip_address"), + device_id=data.get("device_id"), + user_agent=data.get("user_agent"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UrnWorkOSOAuthGrantTypeEmailVerificationCodeSessionAuthenticateRequest: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["client_id"] = self.client_id + result["client_secret"] = self.client_secret + result["grant_type"] = self.grant_type + result["code"] = self.code + result["pending_authentication_token"] = self.pending_authentication_token + if self.ip_address is not None: + result["ip_address"] = self.ip_address + if self.device_id is not None: + result["device_id"] = self.device_id + if self.user_agent is not None: + result["user_agent"] = self.user_agent + return result diff --git a/src/workos/user_management/authentication/models/urn_workos_oauth_grant_type_magic_auth_code_session_authenticate_request.py b/src/workos/user_management/authentication/models/urn_workos_oauth_grant_type_magic_auth_code_session_authenticate_request.py new file mode 100644 index 00000000..975c7f4c --- /dev/null +++ b/src/workos/user_management/authentication/models/urn_workos_oauth_grant_type_magic_auth_code_session_authenticate_request.py @@ -0,0 +1,70 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class UrnWorkOSOAuthGrantTypeMagicAuthCodeSessionAuthenticateRequest: + """Urn Work OSO Auth Grant Type Magic Auth Code Session Authenticate Request model.""" + + client_id: str + """The client ID of the application.""" + client_secret: str + """The client secret of the application.""" + grant_type: Literal["urn:workos:oauth:grant-type:magic-auth:code"] + code: str + """The one-time code for Magic Auth authentication.""" + email: str + """The user's email address.""" + invitation_token: Optional[str] = None + """An invitation token to accept during authentication.""" + ip_address: Optional[str] = None + """The IP address of the user's request.""" + device_id: Optional[str] = None + """A unique identifier for the device.""" + user_agent: Optional[str] = None + """The user agent string from the user's browser.""" + + @classmethod + def from_dict( + cls, data: Dict[str, Any] + ) -> "UrnWorkOSOAuthGrantTypeMagicAuthCodeSessionAuthenticateRequest": + """Deserialize from a dictionary.""" + try: + return cls( + client_id=data["client_id"], + client_secret=data["client_secret"], + grant_type=data["grant_type"], + code=data["code"], + email=data["email"], + invitation_token=data.get("invitation_token"), + ip_address=data.get("ip_address"), + device_id=data.get("device_id"), + user_agent=data.get("user_agent"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UrnWorkOSOAuthGrantTypeMagicAuthCodeSessionAuthenticateRequest: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["client_id"] = self.client_id + result["client_secret"] = self.client_secret + result["grant_type"] = self.grant_type + result["code"] = self.code + result["email"] = self.email + if self.invitation_token is not None: + result["invitation_token"] = self.invitation_token + if self.ip_address is not None: + result["ip_address"] = self.ip_address + if self.device_id is not None: + result["device_id"] = self.device_id + if self.user_agent is not None: + result["user_agent"] = self.user_agent + return result diff --git a/src/workos/user_management/authentication/models/urn_workos_oauth_grant_type_mfa_totp_session_authenticate_request.py b/src/workos/user_management/authentication/models/urn_workos_oauth_grant_type_mfa_totp_session_authenticate_request.py new file mode 100644 index 00000000..df19751d --- /dev/null +++ b/src/workos/user_management/authentication/models/urn_workos_oauth_grant_type_mfa_totp_session_authenticate_request.py @@ -0,0 +1,69 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class UrnWorkOSOAuthGrantTypeMFATotpSessionAuthenticateRequest: + """Urn Work OSO Auth Grant Type MFA Totp Session Authenticate Request model.""" + + client_id: str + """The client ID of the application.""" + client_secret: str + """The client secret of the application.""" + grant_type: Literal["urn:workos:oauth:grant-type:mfa-totp"] + code: str + """The TOTP code from the authenticator app.""" + pending_authentication_token: str + """The pending authentication token from a previous authentication attempt.""" + authentication_challenge_id: str + """The ID of the MFA authentication challenge.""" + ip_address: Optional[str] = None + """The IP address of the user's request.""" + device_id: Optional[str] = None + """A unique identifier for the device.""" + user_agent: Optional[str] = None + """The user agent string from the user's browser.""" + + @classmethod + def from_dict( + cls, data: Dict[str, Any] + ) -> "UrnWorkOSOAuthGrantTypeMFATotpSessionAuthenticateRequest": + """Deserialize from a dictionary.""" + try: + return cls( + client_id=data["client_id"], + client_secret=data["client_secret"], + grant_type=data["grant_type"], + code=data["code"], + pending_authentication_token=data["pending_authentication_token"], + authentication_challenge_id=data["authentication_challenge_id"], + ip_address=data.get("ip_address"), + device_id=data.get("device_id"), + user_agent=data.get("user_agent"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UrnWorkOSOAuthGrantTypeMFATotpSessionAuthenticateRequest: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["client_id"] = self.client_id + result["client_secret"] = self.client_secret + result["grant_type"] = self.grant_type + result["code"] = self.code + result["pending_authentication_token"] = self.pending_authentication_token + result["authentication_challenge_id"] = self.authentication_challenge_id + if self.ip_address is not None: + result["ip_address"] = self.ip_address + if self.device_id is not None: + result["device_id"] = self.device_id + if self.user_agent is not None: + result["user_agent"] = self.user_agent + return result diff --git a/src/workos/user_management/authentication/models/urn_workos_oauth_grant_type_organization_selection_session_authenticate_request.py b/src/workos/user_management/authentication/models/urn_workos_oauth_grant_type_organization_selection_session_authenticate_request.py new file mode 100644 index 00000000..8fd957a5 --- /dev/null +++ b/src/workos/user_management/authentication/models/urn_workos_oauth_grant_type_organization_selection_session_authenticate_request.py @@ -0,0 +1,65 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class UrnWorkOSOAuthGrantTypeOrganizationSelectionSessionAuthenticateRequest: + """Urn Work OSO Auth Grant Type Organization Selection Session Authenticate Request model.""" + + client_id: str + """The client ID of the application.""" + client_secret: str + """The client secret of the application.""" + grant_type: Literal["urn:workos:oauth:grant-type:organization-selection"] + pending_authentication_token: str + """The pending authentication token from a previous authentication attempt.""" + organization_id: str + """The ID of the organization the user selected.""" + ip_address: Optional[str] = None + """The IP address of the user's request.""" + device_id: Optional[str] = None + """A unique identifier for the device.""" + user_agent: Optional[str] = None + """The user agent string from the user's browser.""" + + @classmethod + def from_dict( + cls, data: Dict[str, Any] + ) -> "UrnWorkOSOAuthGrantTypeOrganizationSelectionSessionAuthenticateRequest": + """Deserialize from a dictionary.""" + try: + return cls( + client_id=data["client_id"], + client_secret=data["client_secret"], + grant_type=data["grant_type"], + pending_authentication_token=data["pending_authentication_token"], + organization_id=data["organization_id"], + ip_address=data.get("ip_address"), + device_id=data.get("device_id"), + user_agent=data.get("user_agent"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UrnWorkOSOAuthGrantTypeOrganizationSelectionSessionAuthenticateRequest: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["client_id"] = self.client_id + result["client_secret"] = self.client_secret + result["grant_type"] = self.grant_type + result["pending_authentication_token"] = self.pending_authentication_token + result["organization_id"] = self.organization_id + if self.ip_address is not None: + result["ip_address"] = self.ip_address + if self.device_id is not None: + result["device_id"] = self.device_id + if self.user_agent is not None: + result["user_agent"] = self.user_agent + return result diff --git a/src/workos/user_management/authentication/models/user.py b/src/workos/user_management/authentication/models/user.py new file mode 100644 index 00000000..b8f7cffd --- /dev/null +++ b/src/workos/user_management/authentication/models/user.py @@ -0,0 +1,113 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class User: + """The user object.""" + + object: Literal["user"] + """Distinguishes the user object.""" + id: str + """The unique ID of the user.""" + first_name: Optional[str] + """The first name of the user.""" + last_name: Optional[str] + """The last name of the user.""" + profile_picture_url: Optional[str] + """A URL reference to an image representing the user.""" + email: str + """The email address of the user.""" + email_verified: bool + """Whether the user's email has been verified.""" + external_id: Optional[str] + """The external ID of the user.""" + last_sign_in_at: Optional[datetime] + """The timestamp when the user last signed in.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + metadata: Optional[Dict[str, str]] = None + """Object containing metadata key/value pairs associated with the user.""" + locale: Optional[str] = None + """The user's preferred locale.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "User": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + first_name=data["first_name"], + last_name=data["last_name"], + profile_picture_url=data["profile_picture_url"], + email=data["email"], + email_verified=data["email_verified"], + external_id=data["external_id"], + last_sign_in_at=datetime.fromisoformat(_v.replace("Z", "+00:00")) + if (_v := data["last_sign_in_at"]) is not None + else None, + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + metadata=data.get("metadata"), + locale=data.get("locale"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing User: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + if self.first_name is not None: + result["first_name"] = self.first_name + else: + result["first_name"] = None + if self.last_name is not None: + result["last_name"] = self.last_name + else: + result["last_name"] = None + if self.profile_picture_url is not None: + result["profile_picture_url"] = self.profile_picture_url + else: + result["profile_picture_url"] = None + result["email"] = self.email + result["email_verified"] = self.email_verified + if self.external_id is not None: + result["external_id"] = self.external_id + else: + result["external_id"] = None + if self.last_sign_in_at is not None: + result["last_sign_in_at"] = self.last_sign_in_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + else: + result["last_sign_in_at"] = None + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + if self.metadata is not None: + result["metadata"] = self.metadata + if self.locale is not None: + result["locale"] = self.locale + else: + result["locale"] = None + return result diff --git a/src/workos/user_management/authentication/models/user_management_authentication_provider.py b/src/workos/user_management/authentication/models/user_management_authentication_provider.py new file mode 100644 index 00000000..db92a977 --- /dev/null +++ b/src/workos/user_management/authentication/models/user_management_authentication_provider.py @@ -0,0 +1,35 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of user management authentication provider values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class UserManagementAuthenticationProvider(str, Enum): + """Known values for UserManagementAuthenticationProvider.""" + + AUTHKIT = "authkit" + APPLE_OAUTH = "AppleOAuth" + GIT_HUB_OAUTH = "GitHubOAuth" + GOOGLE_OAUTH = "GoogleOAuth" + MICROSOFT_OAUTH = "MicrosoftOAuth" + + @classmethod + def _missing_( + cls, value: object + ) -> Optional["UserManagementAuthenticationProvider"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +UserManagementAuthenticationProviderLiteral: TypeAlias = Literal[ + "authkit", "AppleOAuth", "GitHubOAuth", "GoogleOAuth", "MicrosoftOAuth" +] diff --git a/src/workos/user_management/authentication/models/user_management_authentication_screen_hint.py b/src/workos/user_management/authentication/models/user_management_authentication_screen_hint.py new file mode 100644 index 00000000..986a4f32 --- /dev/null +++ b/src/workos/user_management/authentication/models/user_management_authentication_screen_hint.py @@ -0,0 +1,30 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Enumeration of user management authentication screen hint values.""" + +from __future__ import annotations + +from enum import Enum +from typing import Optional +from typing_extensions import Literal, TypeAlias + + +class UserManagementAuthenticationScreenHint(str, Enum): + """Known values for UserManagementAuthenticationScreenHint.""" + + SIGN_UP = "sign-up" + SIGN_IN = "sign-in" + + @classmethod + def _missing_( + cls, value: object + ) -> Optional["UserManagementAuthenticationScreenHint"]: + if not isinstance(value, str): + return None + unknown = str.__new__(cls, value) + unknown._name_ = value.upper() + unknown._value_ = value + return unknown + + +UserManagementAuthenticationScreenHintLiteral: TypeAlias = Literal["sign-up", "sign-in"] diff --git a/src/workos/user_management/cors_origins/__init__.py b/src/workos/user_management/cors_origins/__init__.py new file mode 100644 index 00000000..4e919e8a --- /dev/null +++ b/src/workos/user_management/cors_origins/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import UserManagementCorsOrigins, AsyncUserManagementCorsOrigins +from .models import * diff --git a/src/workos/user_management/cors_origins/_resource.py b/src/workos/user_management/cors_origins/_resource.py new file mode 100644 index 00000000..d7c19e9f --- /dev/null +++ b/src/workos/user_management/cors_origins/_resource.py @@ -0,0 +1,95 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Optional + +if TYPE_CHECKING: + from ..._client import AsyncWorkOSClient, WorkOSClient + +from .models import CORSOriginResponse +from ..._types import RequestOptions + + +class UserManagementCorsOrigins: + """User Management Cors Origins API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def create_cors_origin( + self, + *, + origin: str, + request_options: Optional[RequestOptions] = None, + ) -> CORSOriginResponse: + """Create a CORS origin + + Creates a new CORS origin for the current environment. CORS origins allow browser-based applications to make requests to the WorkOS API. + + Args: + origin: The origin URL to allow for CORS requests. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + CORSOriginResponse + + Raises: + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "origin": origin, + } + return self._client.request( + method="post", + path="user_management/cors_origins", + body=body, + model=CORSOriginResponse, + request_options=request_options, + ) + + +class AsyncUserManagementCorsOrigins: + """User Management Cors Origins API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def create_cors_origin( + self, + *, + origin: str, + request_options: Optional[RequestOptions] = None, + ) -> CORSOriginResponse: + """Create a CORS origin + + Creates a new CORS origin for the current environment. CORS origins allow browser-based applications to make requests to the WorkOS API. + + Args: + origin: The origin URL to allow for CORS requests. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + CORSOriginResponse + + Raises: + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "origin": origin, + } + return await self._client.request( + method="post", + path="user_management/cors_origins", + body=body, + model=CORSOriginResponse, + request_options=request_options, + ) diff --git a/src/workos/user_management/cors_origins/models/__init__.py b/src/workos/user_management/cors_origins/models/__init__.py new file mode 100644 index 00000000..82069554 --- /dev/null +++ b/src/workos/user_management/cors_origins/models/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from .cors_origin_response import CORSOriginResponse as CORSOriginResponse +from .create_cors_origin import CreateCORSOrigin as CreateCORSOrigin diff --git a/src/workos/user_management/cors_origins/models/cors_origin_response.py b/src/workos/user_management/cors_origins/models/cors_origin_response.py new file mode 100644 index 00000000..75246149 --- /dev/null +++ b/src/workos/user_management/cors_origins/models/cors_origin_response.py @@ -0,0 +1,58 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Literal +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class CORSOriginResponse: + """CORS Origin Response model.""" + + object: Literal["cors_origin"] + """Distinguishes the CORS origin object.""" + id: str + """Unique identifier of the CORS origin.""" + origin: str + """The origin URL.""" + created_at: datetime + """Timestamp when the CORS origin was created.""" + updated_at: datetime + """Timestamp when the CORS origin was last updated.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CORSOriginResponse": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + origin=data["origin"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing CORSOriginResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["origin"] = self.origin + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + return result diff --git a/src/workos/user_management/cors_origins/models/create_cors_origin.py b/src/workos/user_management/cors_origins/models/create_cors_origin.py new file mode 100644 index 00000000..e5ea8552 --- /dev/null +++ b/src/workos/user_management/cors_origins/models/create_cors_origin.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class CreateCORSOrigin: + """Create CORS Origin model.""" + + origin: str + """The origin URL to allow for CORS requests.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CreateCORSOrigin": + """Deserialize from a dictionary.""" + try: + return cls( + origin=data["origin"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing CreateCORSOrigin: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["origin"] = self.origin + return result diff --git a/src/workos/user_management/data_providers/__init__.py b/src/workos/user_management/data_providers/__init__.py new file mode 100644 index 00000000..e894c3e8 --- /dev/null +++ b/src/workos/user_management/data_providers/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import UserManagementDataProviders, AsyncUserManagementDataProviders +from .models import * diff --git a/src/workos/user_management/data_providers/_resource.py b/src/workos/user_management/data_providers/_resource.py new file mode 100644 index 00000000..789a11eb --- /dev/null +++ b/src/workos/user_management/data_providers/_resource.py @@ -0,0 +1,269 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Optional + +if TYPE_CHECKING: + from ..._client import AsyncWorkOSClient, WorkOSClient + +from .models import ConnectedAccount, DataIntegrationsListResponse +from ..._types import RequestOptions + + +class UserManagementDataProviders: + """User Management Data Providers API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def get_user_data_installation( + self, + user_id: str, + slug: str, + *, + organization_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> ConnectedAccount: + """Get a connected account + + Retrieves a user's [connected account](https://workos.com/docs/reference/pipes/connected-account) for a specific provider. + + Args: + user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier. + slug: The slug identifier of the provider (e.g., `github`, `slack`, `notion`). + organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter if the connection is scoped to an organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + ConnectedAccount + + Raises: + AuthenticationException: If the API key is invalid (401). + NotFoundException: If the resource is not found (404). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params: Dict[str, Any] = { + k: v + for k, v in { + "organization_id": organization_id, + }.items() + if v is not None + } + return self._client.request( + method="get", + path=f"user_management/users/{user_id}/connected_accounts/{slug}", + params=params, + model=ConnectedAccount, + request_options=request_options, + ) + + def delete_user_data_installation( + self, + user_id: str, + slug: str, + *, + organization_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a connected account + + Disconnects WorkOS's account for the user, including removing any stored access and refresh tokens. The user will need to reauthorize if they want to reconnect. This does not revoke access on the provider side. + + Args: + user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier. + slug: The slug identifier of the provider (e.g., `github`, `slack`, `notion`). + organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter if the connection is scoped to an organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + AuthenticationException: If the API key is invalid (401). + NotFoundException: If the resource is not found (404). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params: Dict[str, Any] = { + k: v + for k, v in { + "organization_id": organization_id, + }.items() + if v is not None + } + self._client.request( + method="delete", + path=f"user_management/users/{user_id}/connected_accounts/{slug}", + params=params, + request_options=request_options, + ) + + def get_user_data_integration( + self, + user_id: str, + *, + organization_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> DataIntegrationsListResponse: + """List providers + + Retrieves a list of available providers and the user's connection status for each. Returns all providers configured for your environment, along with the user's [connected account](https://workos.com/docs/reference/pipes/connected-account) information where applicable. + + Args: + user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier to list providers and connected accounts for. + organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to filter connections for a specific organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + DataIntegrationsListResponse + + Raises: + AuthenticationException: If the API key is invalid (401). + NotFoundException: If the resource is not found (404). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params: Dict[str, Any] = { + k: v + for k, v in { + "organization_id": organization_id, + }.items() + if v is not None + } + return self._client.request( + method="get", + path=f"user_management/users/{user_id}/data_providers", + params=params, + model=DataIntegrationsListResponse, + request_options=request_options, + ) + + get_user_data_integrations = get_user_data_integration + + +class AsyncUserManagementDataProviders: + """User Management Data Providers API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def get_user_data_installation( + self, + user_id: str, + slug: str, + *, + organization_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> ConnectedAccount: + """Get a connected account + + Retrieves a user's [connected account](https://workos.com/docs/reference/pipes/connected-account) for a specific provider. + + Args: + user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier. + slug: The slug identifier of the provider (e.g., `github`, `slack`, `notion`). + organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter if the connection is scoped to an organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + ConnectedAccount + + Raises: + AuthenticationException: If the API key is invalid (401). + NotFoundException: If the resource is not found (404). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params: Dict[str, Any] = { + k: v + for k, v in { + "organization_id": organization_id, + }.items() + if v is not None + } + return await self._client.request( + method="get", + path=f"user_management/users/{user_id}/connected_accounts/{slug}", + params=params, + model=ConnectedAccount, + request_options=request_options, + ) + + async def delete_user_data_installation( + self, + user_id: str, + slug: str, + *, + organization_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a connected account + + Disconnects WorkOS's account for the user, including removing any stored access and refresh tokens. The user will need to reauthorize if they want to reconnect. This does not revoke access on the provider side. + + Args: + user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier. + slug: The slug identifier of the provider (e.g., `github`, `slack`, `notion`). + organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter if the connection is scoped to an organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + AuthenticationException: If the API key is invalid (401). + NotFoundException: If the resource is not found (404). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params: Dict[str, Any] = { + k: v + for k, v in { + "organization_id": organization_id, + }.items() + if v is not None + } + await self._client.request( + method="delete", + path=f"user_management/users/{user_id}/connected_accounts/{slug}", + params=params, + request_options=request_options, + ) + + async def get_user_data_integration( + self, + user_id: str, + *, + organization_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> DataIntegrationsListResponse: + """List providers + + Retrieves a list of available providers and the user's connection status for each. Returns all providers configured for your environment, along with the user's [connected account](https://workos.com/docs/reference/pipes/connected-account) information where applicable. + + Args: + user_id: A [User](https://workos.com/docs/reference/authkit/user) identifier to list providers and connected accounts for. + organization_id: An [Organization](https://workos.com/docs/reference/organization) identifier. Optional parameter to filter connections for a specific organization. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + DataIntegrationsListResponse + + Raises: + AuthenticationException: If the API key is invalid (401). + NotFoundException: If the resource is not found (404). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params: Dict[str, Any] = { + k: v + for k, v in { + "organization_id": organization_id, + }.items() + if v is not None + } + return await self._client.request( + method="get", + path=f"user_management/users/{user_id}/data_providers", + params=params, + model=DataIntegrationsListResponse, + request_options=request_options, + ) + + get_user_data_integrations = get_user_data_integration diff --git a/src/workos/user_management/data_providers/models/__init__.py b/src/workos/user_management/data_providers/models/__init__.py new file mode 100644 index 00000000..bd6432fa --- /dev/null +++ b/src/workos/user_management/data_providers/models/__init__.py @@ -0,0 +1,12 @@ +# This file is auto-generated by oagen. Do not edit. + +from .connected_account import ConnectedAccount as ConnectedAccount +from .data_integrations_list_response import ( + DataIntegrationsListResponse as DataIntegrationsListResponse, +) +from .data_integrations_list_response_data import ( + DataIntegrationsListResponseData as DataIntegrationsListResponseData, +) +from .data_integrations_list_response_data_connected_account import ( + DataIntegrationsListResponseDataConnectedAccount as DataIntegrationsListResponseDataConnectedAccount, +) diff --git a/src/workos/user_management/data_providers/models/connected_account.py b/src/workos/user_management/data_providers/models/connected_account.py new file mode 100644 index 00000000..6bdd1b38 --- /dev/null +++ b/src/workos/user_management/data_providers/models/connected_account.py @@ -0,0 +1,71 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, List, Literal, Optional +from workos._errors import BaseRequestException +from workos.common.models import ConnectedAccountState + + +@dataclass(slots=True) +class ConnectedAccount: + """Connected Account model.""" + + object: Literal["connected_account"] + """Distinguishes the connected account object.""" + id: str + """The unique identifier of the connected account.""" + user_id: Optional[str] + """The [User](https://workos.com/docs/reference/authkit/user) identifier associated with this connection.""" + organization_id: Optional[str] + """The [Organization](https://workos.com/docs/reference/organization) identifier associated with this connection, or `null` if not scoped to an organization.""" + scopes: List[str] + """The OAuth scopes granted for this connection.""" + state: "ConnectedAccountState" + """The state of the connected account: +- `connected`: The connection is active and tokens are valid. +- `needs_reauthorization`: The user needs to reauthorize the connection, typically because required scopes have changed. +- `disconnected`: The connection has been disconnected.""" + created_at: str + """The timestamp when the connection was created.""" + updated_at: str + """The timestamp when the connection was last updated.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ConnectedAccount": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + user_id=data["user_id"], + organization_id=data["organization_id"], + scopes=data["scopes"], + state=ConnectedAccountState(data["state"]), + created_at=data["created_at"], + updated_at=data["updated_at"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing ConnectedAccount: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + if self.user_id is not None: + result["user_id"] = self.user_id + else: + result["user_id"] = None + if self.organization_id is not None: + result["organization_id"] = self.organization_id + else: + result["organization_id"] = None + result["scopes"] = self.scopes + result["state"] = self.state + result["created_at"] = self.created_at + result["updated_at"] = self.updated_at + return result diff --git a/src/workos/user_management/data_providers/models/data_integrations_list_response.py b/src/workos/user_management/data_providers/models/data_integrations_list_response.py new file mode 100644 index 00000000..7c48879b --- /dev/null +++ b/src/workos/user_management/data_providers/models/data_integrations_list_response.py @@ -0,0 +1,45 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, List, Literal +from workos._errors import BaseRequestException + +from .data_integrations_list_response_data import DataIntegrationsListResponseData + + +@dataclass(slots=True) +class DataIntegrationsListResponse: + """Data Integrations List Response model.""" + + object: Literal["list"] + """Indicates this is a list response.""" + data: List["DataIntegrationsListResponseData"] + """A list of [providers](https://workos.com/docs/reference/pipes/provider), each including a [`connected_account`](https://workos.com/docs/reference/pipes/connected-account) field with the user's connection status.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "DataIntegrationsListResponse": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + data=[ + DataIntegrationsListResponseData.from_dict( + cast(Dict[str, Any], item) + ) + for item in cast(list[Any], data["data"]) + ], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing DataIntegrationsListResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["data"] = [item.to_dict() for item in self.data] + return result diff --git a/src/workos/user_management/data_providers/models/data_integrations_list_response_data.py b/src/workos/user_management/data_providers/models/data_integrations_list_response_data.py new file mode 100644 index 00000000..5d64cfed --- /dev/null +++ b/src/workos/user_management/data_providers/models/data_integrations_list_response_data.py @@ -0,0 +1,96 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, List, Literal, Optional +from workos._errors import BaseRequestException + +from .data_integrations_list_response_data_connected_account import ( + DataIntegrationsListResponseDataConnectedAccount, +) +from workos.common.models import DataIntegrationsListResponseDataOwnership + + +@dataclass(slots=True) +class DataIntegrationsListResponseData: + """Data Integrations List Response Data model.""" + + object: Literal["data_provider"] + """Distinguishes the data provider object.""" + id: str + """The unique identifier of the provider.""" + name: str + """The display name of the provider (e.g., "GitHub", "Slack").""" + description: Optional[str] + """A description of the provider explaining how it will be used, if configured.""" + slug: str + """The slug identifier used in API calls (e.g., `github`, `slack`, `notion`).""" + integration_type: str + """The type of integration (e.g., `github`, `slack`).""" + credentials_type: str + """The type of credentials used by the provider (e.g., `oauth2`).""" + scopes: Optional[List[str]] + """The OAuth scopes configured for this provider, or `null` if none are configured.""" + ownership: "DataIntegrationsListResponseDataOwnership" + """Whether the provider is owned by a user or organization.""" + created_at: str + """The timestamp when the provider was created.""" + updated_at: str + """The timestamp when the provider was last updated.""" + connected_account: Optional["DataIntegrationsListResponseDataConnectedAccount"] + """The user's [connected account](https://workos.com/docs/reference/pipes/connected-account) for this provider, or `null` if the user has not connected.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "DataIntegrationsListResponseData": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + name=data["name"], + description=data["description"], + slug=data["slug"], + integration_type=data["integration_type"], + credentials_type=data["credentials_type"], + scopes=data["scopes"], + ownership=DataIntegrationsListResponseDataOwnership(data["ownership"]), + created_at=data["created_at"], + updated_at=data["updated_at"], + connected_account=DataIntegrationsListResponseDataConnectedAccount.from_dict( + cast(Dict[str, Any], _v) + ) + if (_v := data["connected_account"]) is not None + else None, + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing DataIntegrationsListResponseData: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["name"] = self.name + if self.description is not None: + result["description"] = self.description + else: + result["description"] = None + result["slug"] = self.slug + result["integration_type"] = self.integration_type + result["credentials_type"] = self.credentials_type + if self.scopes is not None: + result["scopes"] = self.scopes + else: + result["scopes"] = None + result["ownership"] = self.ownership + result["created_at"] = self.created_at + result["updated_at"] = self.updated_at + if self.connected_account is not None: + result["connected_account"] = self.connected_account.to_dict() + else: + result["connected_account"] = None + return result diff --git a/src/workos/user_management/data_providers/models/data_integrations_list_response_data_connected_account.py b/src/workos/user_management/data_providers/models/data_integrations_list_response_data_connected_account.py new file mode 100644 index 00000000..3da81c2f --- /dev/null +++ b/src/workos/user_management/data_providers/models/data_integrations_list_response_data_connected_account.py @@ -0,0 +1,82 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, List, Literal, Optional +from workos._errors import BaseRequestException +from workos.common.models import DataIntegrationsListResponseDataConnectedAccountState + + +@dataclass(slots=True) +class DataIntegrationsListResponseDataConnectedAccount: + """Data Integrations List Response Data Connected Account model.""" + + object: Literal["connected_account"] + """Distinguishes the connected account object.""" + id: str + """The unique identifier of the connected account.""" + user_id: Optional[str] + """The [User](https://workos.com/docs/reference/authkit/user) identifier associated with this connection.""" + organization_id: Optional[str] + """The [Organization](https://workos.com/docs/reference/organization) identifier associated with this connection, or `null` if not scoped to an organization.""" + scopes: List[str] + """The OAuth scopes granted for this connection.""" + state: "DataIntegrationsListResponseDataConnectedAccountState" + """The state of the connected account: +- `connected`: The connection is active and tokens are valid. +- `needs_reauthorization`: The user needs to reauthorize the connection, typically because required scopes have changed. +- `disconnected`: The connection has been disconnected.""" + created_at: str + """The timestamp when the connection was created.""" + updated_at: str + """The timestamp when the connection was last updated.""" + userland_user_id: Optional[str] = None + """Use `user_id` instead.""" + + @classmethod + def from_dict( + cls, data: Dict[str, Any] + ) -> "DataIntegrationsListResponseDataConnectedAccount": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + user_id=data["user_id"], + organization_id=data["organization_id"], + scopes=data["scopes"], + state=DataIntegrationsListResponseDataConnectedAccountState( + data["state"] + ), + created_at=data["created_at"], + updated_at=data["updated_at"], + userland_user_id=data.get("userlandUserId"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing DataIntegrationsListResponseDataConnectedAccount: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + if self.user_id is not None: + result["user_id"] = self.user_id + else: + result["user_id"] = None + if self.organization_id is not None: + result["organization_id"] = self.organization_id + else: + result["organization_id"] = None + result["scopes"] = self.scopes + result["state"] = self.state + result["created_at"] = self.created_at + result["updated_at"] = self.updated_at + if self.userland_user_id is not None: + result["userlandUserId"] = self.userland_user_id + else: + result["userlandUserId"] = None + return result diff --git a/src/workos/user_management/invitations/__init__.py b/src/workos/user_management/invitations/__init__.py new file mode 100644 index 00000000..55fd2cba --- /dev/null +++ b/src/workos/user_management/invitations/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import UserManagementInvitations, AsyncUserManagementInvitations +from .models import * diff --git a/src/workos/user_management/invitations/_resource.py b/src/workos/user_management/invitations/_resource.py new file mode 100644 index 00000000..767408a0 --- /dev/null +++ b/src/workos/user_management/invitations/_resource.py @@ -0,0 +1,573 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Optional + +if TYPE_CHECKING: + from ..._client import AsyncWorkOSClient, WorkOSClient + +from .models import Invitation, UserInvite +from .models import UserManagementInvitationsOrder +from workos.common.models import ( + CreateUserInviteOptionsDtoLocale, + ResendUserInviteOptionsDtoLocale, +) +from ..._pagination import AsyncPage, SyncPage +from ..._types import RequestOptions + + +class UserManagementInvitations: + """User Management Invitations API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[UserManagementInvitationsOrder] = None, + organization_id: Optional[str] = None, + email: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[UserInvite]: + """List invitations + + Get a list of all of invitations matching the criteria specified. + + Args: + organization_id: The ID of the [organization](https://workos.com/docs/reference/organization) that the recipient will join. + email: The email address of the recipient. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[UserInvite] + + Raises: + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "organization_id": organization_id, + "email": email, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path="user_management/invitations", + model=UserInvite, + params=params, + request_options=request_options, + ) + + def create( + self, + *, + email: str, + organization_id: Optional[str] = None, + role_slug: Optional[str] = None, + expires_in_days: Optional[int] = None, + inviter_user_id: Optional[str] = None, + locale: Optional[CreateUserInviteOptionsDtoLocale] = None, + request_options: Optional[RequestOptions] = None, + ) -> UserInvite: + """Send an invitation + + Sends an invitation email to the recipient. + + Args: + email: The email address of the recipient. + organization_id: The ID of the [organization](https://workos.com/docs/reference/organization) that the recipient will join. + role_slug: The [role](https://workos.com/docs/authkit/roles) that the recipient will receive when they join the organization in the invitation. + expires_in_days: How many days the invitations will be valid for. Must be between 1 and 30 days. Defaults to 7 days if not specified. + inviter_user_id: The ID of the [user](https://workos.com/docs/reference/authkit/user) who invites the recipient. The invitation email will mention the name of this user. + locale: The locale to use when rendering the invitation email. See [supported locales](https://workos.com/docs/authkit/hosted-ui/localization). + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + UserInvite + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "email": email, + "organization_id": organization_id, + "role_slug": role_slug, + "expires_in_days": expires_in_days, + "inviter_user_id": inviter_user_id, + "locale": locale, + }.items() + if v is not None + } + return self._client.request( + method="post", + path="user_management/invitations", + body=body, + model=UserInvite, + request_options=request_options, + ) + + def get_by_token( + self, + token: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> UserInvite: + """Find an invitation by token + + Retrieve an existing invitation using the token. + + Args: + token: The token used to accept the invitation. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + UserInvite + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"user_management/invitations/by_token/{token}", + model=UserInvite, + request_options=request_options, + ) + + def get( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> UserInvite: + """Get an invitation + + Get the details of an existing invitation. + + Args: + id: The unique ID of the invitation. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + UserInvite + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"user_management/invitations/{id}", + model=UserInvite, + request_options=request_options, + ) + + def accept( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Invitation: + """Accept an invitation + + Accepts an invitation and, if linked to an organization, activates the user's membership in that organization. + + Args: + id: The unique ID of the invitation. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Invitation + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="post", + path=f"user_management/invitations/{id}/accept", + model=Invitation, + request_options=request_options, + ) + + def resend( + self, + id: str, + *, + locale: Optional[ResendUserInviteOptionsDtoLocale] = None, + request_options: Optional[RequestOptions] = None, + ) -> UserInvite: + """Resend an invitation + + Resends an invitation email to the recipient. The invitation must be in a pending state. + + Args: + id: The unique ID of the invitation. + locale: The locale to use when rendering the invitation email. See [supported locales](https://workos.com/docs/authkit/hosted-ui/localization). + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + UserInvite + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "locale": locale, + }.items() + if v is not None + } + return self._client.request( + method="post", + path=f"user_management/invitations/{id}/resend", + body=body, + model=UserInvite, + request_options=request_options, + ) + + def revoke( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Invitation: + """Revoke an invitation + + Revokes an existing invitation. + + Args: + id: The unique ID of the invitation. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Invitation + + Raises: + BadRequestException: If the request is malformed (400). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="post", + path=f"user_management/invitations/{id}/revoke", + model=Invitation, + request_options=request_options, + ) + + +class AsyncUserManagementInvitations: + """User Management Invitations API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[UserManagementInvitationsOrder] = None, + organization_id: Optional[str] = None, + email: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[UserInvite]: + """List invitations + + Get a list of all of invitations matching the criteria specified. + + Args: + organization_id: The ID of the [organization](https://workos.com/docs/reference/organization) that the recipient will join. + email: The email address of the recipient. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[UserInvite] + + Raises: + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "organization_id": organization_id, + "email": email, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path="user_management/invitations", + model=UserInvite, + params=params, + request_options=request_options, + ) + + async def create( + self, + *, + email: str, + organization_id: Optional[str] = None, + role_slug: Optional[str] = None, + expires_in_days: Optional[int] = None, + inviter_user_id: Optional[str] = None, + locale: Optional[CreateUserInviteOptionsDtoLocale] = None, + request_options: Optional[RequestOptions] = None, + ) -> UserInvite: + """Send an invitation + + Sends an invitation email to the recipient. + + Args: + email: The email address of the recipient. + organization_id: The ID of the [organization](https://workos.com/docs/reference/organization) that the recipient will join. + role_slug: The [role](https://workos.com/docs/authkit/roles) that the recipient will receive when they join the organization in the invitation. + expires_in_days: How many days the invitations will be valid for. Must be between 1 and 30 days. Defaults to 7 days if not specified. + inviter_user_id: The ID of the [user](https://workos.com/docs/reference/authkit/user) who invites the recipient. The invitation email will mention the name of this user. + locale: The locale to use when rendering the invitation email. See [supported locales](https://workos.com/docs/authkit/hosted-ui/localization). + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + UserInvite + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "email": email, + "organization_id": organization_id, + "role_slug": role_slug, + "expires_in_days": expires_in_days, + "inviter_user_id": inviter_user_id, + "locale": locale, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path="user_management/invitations", + body=body, + model=UserInvite, + request_options=request_options, + ) + + async def get_by_token( + self, + token: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> UserInvite: + """Find an invitation by token + + Retrieve an existing invitation using the token. + + Args: + token: The token used to accept the invitation. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + UserInvite + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"user_management/invitations/by_token/{token}", + model=UserInvite, + request_options=request_options, + ) + + async def get( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> UserInvite: + """Get an invitation + + Get the details of an existing invitation. + + Args: + id: The unique ID of the invitation. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + UserInvite + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"user_management/invitations/{id}", + model=UserInvite, + request_options=request_options, + ) + + async def accept( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Invitation: + """Accept an invitation + + Accepts an invitation and, if linked to an organization, activates the user's membership in that organization. + + Args: + id: The unique ID of the invitation. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Invitation + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="post", + path=f"user_management/invitations/{id}/accept", + model=Invitation, + request_options=request_options, + ) + + async def resend( + self, + id: str, + *, + locale: Optional[ResendUserInviteOptionsDtoLocale] = None, + request_options: Optional[RequestOptions] = None, + ) -> UserInvite: + """Resend an invitation + + Resends an invitation email to the recipient. The invitation must be in a pending state. + + Args: + id: The unique ID of the invitation. + locale: The locale to use when rendering the invitation email. See [supported locales](https://workos.com/docs/authkit/hosted-ui/localization). + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + UserInvite + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "locale": locale, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path=f"user_management/invitations/{id}/resend", + body=body, + model=UserInvite, + request_options=request_options, + ) + + async def revoke( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> Invitation: + """Revoke an invitation + + Revokes an existing invitation. + + Args: + id: The unique ID of the invitation. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + Invitation + + Raises: + BadRequestException: If the request is malformed (400). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="post", + path=f"user_management/invitations/{id}/revoke", + model=Invitation, + request_options=request_options, + ) diff --git a/src/workos/user_management/invitations/models/__init__.py b/src/workos/user_management/invitations/models/__init__.py new file mode 100644 index 00000000..1b905624 --- /dev/null +++ b/src/workos/user_management/invitations/models/__init__.py @@ -0,0 +1,13 @@ +# This file is auto-generated by oagen. Do not edit. + +from .create_user_invite_options import ( + CreateUserInviteOptions as CreateUserInviteOptions, +) +from .invitation import Invitation as Invitation +from .resend_user_invite_options import ( + ResendUserInviteOptions as ResendUserInviteOptions, +) +from .user_invite import UserInvite as UserInvite +from .user_management_invitations_order import ( + UserManagementInvitationsOrder as UserManagementInvitationsOrder, +) diff --git a/src/workos/user_management/invitations/models/create_user_invite_options.py b/src/workos/user_management/invitations/models/create_user_invite_options.py new file mode 100644 index 00000000..dcfaaee1 --- /dev/null +++ b/src/workos/user_management/invitations/models/create_user_invite_options.py @@ -0,0 +1,61 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException +from workos.common.models import CreateUserInviteOptionsDtoLocale + + +@dataclass(slots=True) +class CreateUserInviteOptions: + """Create User Invite Options model.""" + + email: str + """The email address of the recipient.""" + organization_id: Optional[str] = None + """The ID of the [organization](https://workos.com/docs/reference/organization) that the recipient will join.""" + role_slug: Optional[str] = None + """The [role](https://workos.com/docs/authkit/roles) that the recipient will receive when they join the organization in the invitation.""" + expires_in_days: Optional[int] = None + """How many days the invitations will be valid for. Must be between 1 and 30 days. Defaults to 7 days if not specified.""" + inviter_user_id: Optional[str] = None + """The ID of the [user](https://workos.com/docs/reference/authkit/user) who invites the recipient. The invitation email will mention the name of this user.""" + locale: Optional["CreateUserInviteOptionsDtoLocale"] = None + """The locale to use when rendering the invitation email. See [supported locales](https://workos.com/docs/authkit/hosted-ui/localization).""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CreateUserInviteOptions": + """Deserialize from a dictionary.""" + try: + return cls( + email=data["email"], + organization_id=data.get("organization_id"), + role_slug=data.get("role_slug"), + expires_in_days=data.get("expires_in_days"), + inviter_user_id=data.get("inviter_user_id"), + locale=CreateUserInviteOptionsDtoLocale(_v) + if (_v := data.get("locale")) is not None + else None, + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing CreateUserInviteOptions: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["email"] = self.email + if self.organization_id is not None: + result["organization_id"] = self.organization_id + if self.role_slug is not None: + result["role_slug"] = self.role_slug + if self.expires_in_days is not None: + result["expires_in_days"] = self.expires_in_days + if self.inviter_user_id is not None: + result["inviter_user_id"] = self.inviter_user_id + if self.locale is not None: + result["locale"] = self.locale + return result diff --git a/src/workos/user_management/invitations/models/invitation.py b/src/workos/user_management/invitations/models/invitation.py new file mode 100644 index 00000000..a54aad58 --- /dev/null +++ b/src/workos/user_management/invitations/models/invitation.py @@ -0,0 +1,122 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException +from workos.common.models import InvitationState + + +@dataclass(slots=True) +class Invitation: + """Invitation model.""" + + object: Literal["invitation"] + """Distinguishes the invitation object.""" + id: str + """The unique ID of the invitation.""" + email: str + """The email address of the recipient.""" + state: "InvitationState" + """The state of the invitation.""" + accepted_at: Optional[datetime] + """The timestamp when the invitation was accepted, or null if not yet accepted.""" + revoked_at: Optional[datetime] + """The timestamp when the invitation was revoked, or null if not revoked.""" + expires_at: datetime + """The timestamp when the invitation expires.""" + organization_id: Optional[str] + """The ID of the [organization](https://workos.com/docs/reference/organization) that the recipient will join.""" + inviter_user_id: Optional[str] + """The ID of the user who invited the recipient, if provided.""" + accepted_user_id: Optional[str] + """The ID of the user who accepted the invitation, once accepted.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + token: str + """The token used to accept the invitation.""" + accept_invitation_url: str + """The URL where the recipient can accept the invitation.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Invitation": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + email=data["email"], + state=InvitationState(data["state"]), + accepted_at=datetime.fromisoformat(_v.replace("Z", "+00:00")) + if (_v := data["accepted_at"]) is not None + else None, + revoked_at=datetime.fromisoformat(_v.replace("Z", "+00:00")) + if (_v := data["revoked_at"]) is not None + else None, + expires_at=datetime.fromisoformat( + data["expires_at"].replace("Z", "+00:00") + ), + organization_id=data["organization_id"], + inviter_user_id=data["inviter_user_id"], + accepted_user_id=data["accepted_user_id"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + token=data["token"], + accept_invitation_url=data["accept_invitation_url"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing Invitation: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["email"] = self.email + result["state"] = self.state + if self.accepted_at is not None: + result["accepted_at"] = self.accepted_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + else: + result["accepted_at"] = None + if self.revoked_at is not None: + result["revoked_at"] = self.revoked_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + else: + result["revoked_at"] = None + result["expires_at"] = self.expires_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + if self.organization_id is not None: + result["organization_id"] = self.organization_id + else: + result["organization_id"] = None + if self.inviter_user_id is not None: + result["inviter_user_id"] = self.inviter_user_id + else: + result["inviter_user_id"] = None + if self.accepted_user_id is not None: + result["accepted_user_id"] = self.accepted_user_id + else: + result["accepted_user_id"] = None + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["token"] = self.token + result["accept_invitation_url"] = self.accept_invitation_url + return result diff --git a/src/workos/user_management/invitations/models/resend_user_invite_options.py b/src/workos/user_management/invitations/models/resend_user_invite_options.py new file mode 100644 index 00000000..923ecb84 --- /dev/null +++ b/src/workos/user_management/invitations/models/resend_user_invite_options.py @@ -0,0 +1,37 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException +from workos.common.models import ResendUserInviteOptionsDtoLocale + + +@dataclass(slots=True) +class ResendUserInviteOptions: + """Resend User Invite Options model.""" + + locale: Optional["ResendUserInviteOptionsDtoLocale"] = None + """The locale to use when rendering the invitation email. See [supported locales](https://workos.com/docs/authkit/hosted-ui/localization).""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ResendUserInviteOptions": + """Deserialize from a dictionary.""" + try: + return cls( + locale=ResendUserInviteOptionsDtoLocale(_v) + if (_v := data.get("locale")) is not None + else None, + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing ResendUserInviteOptions: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + if self.locale is not None: + result["locale"] = self.locale + return result diff --git a/src/workos/user_management/invitations/models/user_invite.py b/src/workos/user_management/invitations/models/user_invite.py new file mode 100644 index 00000000..74fa91f0 --- /dev/null +++ b/src/workos/user_management/invitations/models/user_invite.py @@ -0,0 +1,122 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException +from workos.common.models import UserInviteState + + +@dataclass(slots=True) +class UserInvite: + """User Invite model.""" + + object: Literal["invitation"] + """Distinguishes the invitation object.""" + id: str + """The unique ID of the invitation.""" + email: str + """The email address of the recipient.""" + state: "UserInviteState" + """The state of the invitation.""" + accepted_at: Optional[datetime] + """The timestamp when the invitation was accepted, or null if not yet accepted.""" + revoked_at: Optional[datetime] + """The timestamp when the invitation was revoked, or null if not revoked.""" + expires_at: datetime + """The timestamp when the invitation expires.""" + organization_id: Optional[str] + """The ID of the [organization](https://workos.com/docs/reference/organization) that the recipient will join.""" + inviter_user_id: Optional[str] + """The ID of the user who invited the recipient, if provided.""" + accepted_user_id: Optional[str] + """The ID of the user who accepted the invitation, once accepted.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + token: str + """The token used to accept the invitation.""" + accept_invitation_url: str + """The URL where the recipient can accept the invitation.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "UserInvite": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + email=data["email"], + state=UserInviteState(data["state"]), + accepted_at=datetime.fromisoformat(_v.replace("Z", "+00:00")) + if (_v := data["accepted_at"]) is not None + else None, + revoked_at=datetime.fromisoformat(_v.replace("Z", "+00:00")) + if (_v := data["revoked_at"]) is not None + else None, + expires_at=datetime.fromisoformat( + data["expires_at"].replace("Z", "+00:00") + ), + organization_id=data["organization_id"], + inviter_user_id=data["inviter_user_id"], + accepted_user_id=data["accepted_user_id"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + token=data["token"], + accept_invitation_url=data["accept_invitation_url"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UserInvite: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["email"] = self.email + result["state"] = self.state + if self.accepted_at is not None: + result["accepted_at"] = self.accepted_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + else: + result["accepted_at"] = None + if self.revoked_at is not None: + result["revoked_at"] = self.revoked_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + else: + result["revoked_at"] = None + result["expires_at"] = self.expires_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + if self.organization_id is not None: + result["organization_id"] = self.organization_id + else: + result["organization_id"] = None + if self.inviter_user_id is not None: + result["inviter_user_id"] = self.inviter_user_id + else: + result["inviter_user_id"] = None + if self.accepted_user_id is not None: + result["accepted_user_id"] = self.accepted_user_id + else: + result["accepted_user_id"] = None + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["token"] = self.token + result["accept_invitation_url"] = self.accept_invitation_url + return result diff --git a/src/workos/user_management/invitations/models/user_management_invitations_order.py b/src/workos/user_management/invitations/models/user_management_invitations_order.py new file mode 100644 index 00000000..46b3ab36 --- /dev/null +++ b/src/workos/user_management/invitations/models/user_management_invitations_order.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import ApplicationsOrder + +UserManagementInvitationsOrder = ApplicationsOrder +__all__ = ["UserManagementInvitationsOrder"] diff --git a/src/workos/user_management/jwt_template/__init__.py b/src/workos/user_management/jwt_template/__init__.py new file mode 100644 index 00000000..160b95f1 --- /dev/null +++ b/src/workos/user_management/jwt_template/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import UserManagementJWTTemplate, AsyncUserManagementJWTTemplate +from .models import * diff --git a/src/workos/user_management/jwt_template/_resource.py b/src/workos/user_management/jwt_template/_resource.py new file mode 100644 index 00000000..9139c0b4 --- /dev/null +++ b/src/workos/user_management/jwt_template/_resource.py @@ -0,0 +1,93 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Optional + +if TYPE_CHECKING: + from ..._client import AsyncWorkOSClient, WorkOSClient + +from .models import JWTTemplateResponse +from ..._types import RequestOptions + + +class UserManagementJWTTemplate: + """User Management JWT Template API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def update_jwt_template( + self, + *, + content: str, + request_options: Optional[RequestOptions] = None, + ) -> JWTTemplateResponse: + """Update JWT template + + Update the JWT template for the current environment. + + Args: + content: The JWT template content as a Liquid template string. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + JWTTemplateResponse + + Raises: + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "content": content, + } + return self._client.request( + method="put", + path="user_management/jwt_template", + body=body, + model=JWTTemplateResponse, + request_options=request_options, + ) + + +class AsyncUserManagementJWTTemplate: + """User Management JWT Template API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def update_jwt_template( + self, + *, + content: str, + request_options: Optional[RequestOptions] = None, + ) -> JWTTemplateResponse: + """Update JWT template + + Update the JWT template for the current environment. + + Args: + content: The JWT template content as a Liquid template string. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + JWTTemplateResponse + + Raises: + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "content": content, + } + return await self._client.request( + method="put", + path="user_management/jwt_template", + body=body, + model=JWTTemplateResponse, + request_options=request_options, + ) diff --git a/src/workos/user_management/jwt_template/models/__init__.py b/src/workos/user_management/jwt_template/models/__init__.py new file mode 100644 index 00000000..d4bdb061 --- /dev/null +++ b/src/workos/user_management/jwt_template/models/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from .jwt_template_response import JWTTemplateResponse as JWTTemplateResponse +from .update_jwt_template import UpdateJWTTemplate as UpdateJWTTemplate diff --git a/src/workos/user_management/jwt_template/models/jwt_template_response.py b/src/workos/user_management/jwt_template/models/jwt_template_response.py new file mode 100644 index 00000000..7e5c671b --- /dev/null +++ b/src/workos/user_management/jwt_template/models/jwt_template_response.py @@ -0,0 +1,45 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Literal +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class JWTTemplateResponse: + """JWT Template Response model.""" + + object: Literal["jwt_template"] + """The object type.""" + content: str + """The JWT template content as a Liquid template string.""" + created_at: str + """The timestamp when the JWT template was created.""" + updated_at: str + """The timestamp when the JWT template was last updated.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "JWTTemplateResponse": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + content=data["content"], + created_at=data["created_at"], + updated_at=data["updated_at"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing JWTTemplateResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["content"] = self.content + result["created_at"] = self.created_at + result["updated_at"] = self.updated_at + return result diff --git a/src/workos/user_management/jwt_template/models/update_jwt_template.py b/src/workos/user_management/jwt_template/models/update_jwt_template.py new file mode 100644 index 00000000..d2dbbe75 --- /dev/null +++ b/src/workos/user_management/jwt_template/models/update_jwt_template.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class UpdateJWTTemplate: + """Update JWT Template model.""" + + content: str + """The JWT template content as a Liquid template string.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "UpdateJWTTemplate": + """Deserialize from a dictionary.""" + try: + return cls( + content=data["content"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UpdateJWTTemplate: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["content"] = self.content + return result diff --git a/src/workos/user_management/magic_auth/__init__.py b/src/workos/user_management/magic_auth/__init__.py new file mode 100644 index 00000000..ab683cfb --- /dev/null +++ b/src/workos/user_management/magic_auth/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import UserManagementMagicAuth, AsyncUserManagementMagicAuth +from .models import * diff --git a/src/workos/user_management/magic_auth/_resource.py b/src/workos/user_management/magic_auth/_resource.py new file mode 100644 index 00000000..a62b7c3a --- /dev/null +++ b/src/workos/user_management/magic_auth/_resource.py @@ -0,0 +1,169 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Optional + +if TYPE_CHECKING: + from ..._client import AsyncWorkOSClient, WorkOSClient + +from .models import MagicAuth +from ..._types import RequestOptions + + +class UserManagementMagicAuth: + """User Management Magic Auth API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def send_magic_auth_code_and_return( + self, + *, + email: str, + invitation_token: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> MagicAuth: + """Create a Magic Auth code + + Creates a one-time authentication code that can be sent to the user's email address. The code expires in 10 minutes. To verify the code, [authenticate the user with Magic Auth](https://workos.com/docs/reference/authkit/authentication/magic-auth). + + Args: + email: The email address to send the magic code to. + invitation_token: The invitation token to associate with this magic code. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + MagicAuth + + Raises: + BadRequestException: If the request is malformed (400). + UnprocessableEntityException: If the request data is unprocessable (422). + RateLimitExceededException: If rate limited (429). + AuthenticationException: If the API key is invalid (401). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "email": email, + "invitation_token": invitation_token, + }.items() + if v is not None + } + return self._client.request( + method="post", + path="user_management/magic_auth", + body=body, + model=MagicAuth, + request_options=request_options, + ) + + def get( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> MagicAuth: + """Get Magic Auth code details + + Get the details of an existing [Magic Auth](https://workos.com/docs/reference/authkit/magic-auth) code that can be used to send an email to a user for authentication. + + Args: + id: The unique ID of the Magic Auth code. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + MagicAuth + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"user_management/magic_auth/{id}", + model=MagicAuth, + request_options=request_options, + ) + + +class AsyncUserManagementMagicAuth: + """User Management Magic Auth API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def send_magic_auth_code_and_return( + self, + *, + email: str, + invitation_token: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> MagicAuth: + """Create a Magic Auth code + + Creates a one-time authentication code that can be sent to the user's email address. The code expires in 10 minutes. To verify the code, [authenticate the user with Magic Auth](https://workos.com/docs/reference/authkit/authentication/magic-auth). + + Args: + email: The email address to send the magic code to. + invitation_token: The invitation token to associate with this magic code. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + MagicAuth + + Raises: + BadRequestException: If the request is malformed (400). + UnprocessableEntityException: If the request data is unprocessable (422). + RateLimitExceededException: If rate limited (429). + AuthenticationException: If the API key is invalid (401). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "email": email, + "invitation_token": invitation_token, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path="user_management/magic_auth", + body=body, + model=MagicAuth, + request_options=request_options, + ) + + async def get( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> MagicAuth: + """Get Magic Auth code details + + Get the details of an existing [Magic Auth](https://workos.com/docs/reference/authkit/magic-auth) code that can be used to send an email to a user for authentication. + + Args: + id: The unique ID of the Magic Auth code. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + MagicAuth + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"user_management/magic_auth/{id}", + model=MagicAuth, + request_options=request_options, + ) diff --git a/src/workos/user_management/magic_auth/models/__init__.py b/src/workos/user_management/magic_auth/models/__init__.py new file mode 100644 index 00000000..c136080b --- /dev/null +++ b/src/workos/user_management/magic_auth/models/__init__.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .create_magic_code_and_return import ( + CreateMagicCodeAndReturn as CreateMagicCodeAndReturn, +) +from .magic_auth import MagicAuth as MagicAuth diff --git a/src/workos/user_management/magic_auth/models/create_magic_code_and_return.py b/src/workos/user_management/magic_auth/models/create_magic_code_and_return.py new file mode 100644 index 00000000..51b1e87e --- /dev/null +++ b/src/workos/user_management/magic_auth/models/create_magic_code_and_return.py @@ -0,0 +1,38 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class CreateMagicCodeAndReturn: + """Create Magic Code And Return model.""" + + email: str + """The email address to send the magic code to.""" + invitation_token: Optional[str] = None + """The invitation token to associate with this magic code.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CreateMagicCodeAndReturn": + """Deserialize from a dictionary.""" + try: + return cls( + email=data["email"], + invitation_token=data.get("invitation_token"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing CreateMagicCodeAndReturn: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["email"] = self.email + if self.invitation_token is not None: + result["invitation_token"] = self.invitation_token + return result diff --git a/src/workos/user_management/magic_auth/models/magic_auth.py b/src/workos/user_management/magic_auth/models/magic_auth.py new file mode 100644 index 00000000..40f63ebe --- /dev/null +++ b/src/workos/user_management/magic_auth/models/magic_auth.py @@ -0,0 +1,74 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Literal +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class MagicAuth: + """Magic Auth model.""" + + object: Literal["magic_auth"] + """Distinguishes the Magic Auth object.""" + id: str + """The unique ID of the Magic Auth code.""" + user_id: str + """The unique ID of the user.""" + email: str + """The email address of the user.""" + expires_at: datetime + """The timestamp when the Magic Auth code expires.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + code: str + """The code used to verify the Magic Auth code.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "MagicAuth": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + user_id=data["user_id"], + email=data["email"], + expires_at=datetime.fromisoformat( + data["expires_at"].replace("Z", "+00:00") + ), + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + code=data["code"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing MagicAuth: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["user_id"] = self.user_id + result["email"] = self.email + result["expires_at"] = self.expires_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["code"] = self.code + return result diff --git a/src/workos/user_management/multi_factor_authentication/__init__.py b/src/workos/user_management/multi_factor_authentication/__init__.py new file mode 100644 index 00000000..cccf70b4 --- /dev/null +++ b/src/workos/user_management/multi_factor_authentication/__init__.py @@ -0,0 +1,7 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import ( + UserManagementMultiFactorAuthentication, + AsyncUserManagementMultiFactorAuthentication, +) +from .models import * diff --git a/src/workos/user_management/multi_factor_authentication/_resource.py b/src/workos/user_management/multi_factor_authentication/_resource.py new file mode 100644 index 00000000..d83021af --- /dev/null +++ b/src/workos/user_management/multi_factor_authentication/_resource.py @@ -0,0 +1,224 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Literal, Optional + +if TYPE_CHECKING: + from ..._client import AsyncWorkOSClient, WorkOSClient + +from .models import UserAuthenticationFactorEnrollResponse +from workos.multi_factor_auth.models import AuthenticationFactor +from .models import UserManagementMultiFactorAuthenticationOrder +from ..._pagination import AsyncPage, SyncPage +from ..._types import RequestOptions + + +class UserManagementMultiFactorAuthentication: + """User Management Multi Factor Authentication API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def list( + self, + userland_user_id: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[UserManagementMultiFactorAuthenticationOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[AuthenticationFactor]: + """List authentication factors + + Lists the [authentication factors](https://workos.com/docs/reference/authkit/mfa/authentication-factor) for a user. + + Args: + userland_user_id: The ID of the [user](https://workos.com/docs/reference/authkit/user). + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[AuthenticationFactor] + + Raises: + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path=f"user_management/users/{userland_user_id}/auth_factors", + model=AuthenticationFactor, + params=params, + request_options=request_options, + ) + + def create( + self, + userland_user_id: str, + *, + type: Literal["totp"], + totp_issuer: Optional[str] = None, + totp_user: Optional[str] = None, + totp_secret: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> UserAuthenticationFactorEnrollResponse: + """Enroll an authentication factor + + Enrolls a user in a new [authentication factor](https://workos.com/docs/reference/authkit/mfa/authentication-factor). + + Args: + userland_user_id: The ID of the [user](https://workos.com/docs/reference/authkit/user). + type: The type of the factor to enroll. + totp_issuer: Your application or company name displayed in the user's authenticator app. + totp_user: The user's account name displayed in their authenticator app. + totp_secret: The Base32-encoded shared secret for TOTP factors. This can be provided when creating the auth factor, otherwise it will be generated. The algorithm used to derive TOTP codes is SHA-1, the code length is 6 digits, and the timestep is 30 seconds – the secret must be compatible with these parameters. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + UserAuthenticationFactorEnrollResponse + + Raises: + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "type": type, + "totp_issuer": totp_issuer, + "totp_user": totp_user, + "totp_secret": totp_secret, + }.items() + if v is not None + } + return self._client.request( + method="post", + path=f"user_management/users/{userland_user_id}/auth_factors", + body=body, + model=UserAuthenticationFactorEnrollResponse, + request_options=request_options, + ) + + +class AsyncUserManagementMultiFactorAuthentication: + """User Management Multi Factor Authentication API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def list( + self, + userland_user_id: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[UserManagementMultiFactorAuthenticationOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[AuthenticationFactor]: + """List authentication factors + + Lists the [authentication factors](https://workos.com/docs/reference/authkit/mfa/authentication-factor) for a user. + + Args: + userland_user_id: The ID of the [user](https://workos.com/docs/reference/authkit/user). + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[AuthenticationFactor] + + Raises: + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path=f"user_management/users/{userland_user_id}/auth_factors", + model=AuthenticationFactor, + params=params, + request_options=request_options, + ) + + async def create( + self, + userland_user_id: str, + *, + type: Literal["totp"], + totp_issuer: Optional[str] = None, + totp_user: Optional[str] = None, + totp_secret: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> UserAuthenticationFactorEnrollResponse: + """Enroll an authentication factor + + Enrolls a user in a new [authentication factor](https://workos.com/docs/reference/authkit/mfa/authentication-factor). + + Args: + userland_user_id: The ID of the [user](https://workos.com/docs/reference/authkit/user). + type: The type of the factor to enroll. + totp_issuer: Your application or company name displayed in the user's authenticator app. + totp_user: The user's account name displayed in their authenticator app. + totp_secret: The Base32-encoded shared secret for TOTP factors. This can be provided when creating the auth factor, otherwise it will be generated. The algorithm used to derive TOTP codes is SHA-1, the code length is 6 digits, and the timestep is 30 seconds – the secret must be compatible with these parameters. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + UserAuthenticationFactorEnrollResponse + + Raises: + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "type": type, + "totp_issuer": totp_issuer, + "totp_user": totp_user, + "totp_secret": totp_secret, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path=f"user_management/users/{userland_user_id}/auth_factors", + body=body, + model=UserAuthenticationFactorEnrollResponse, + request_options=request_options, + ) diff --git a/src/workos/user_management/multi_factor_authentication/models/__init__.py b/src/workos/user_management/multi_factor_authentication/models/__init__.py new file mode 100644 index 00000000..8306d8c1 --- /dev/null +++ b/src/workos/user_management/multi_factor_authentication/models/__init__.py @@ -0,0 +1,11 @@ +# This file is auto-generated by oagen. Do not edit. + +from .enroll_user_authentication_factor import ( + EnrollUserAuthenticationFactor as EnrollUserAuthenticationFactor, +) +from .user_authentication_factor_enroll_response import ( + UserAuthenticationFactorEnrollResponse as UserAuthenticationFactorEnrollResponse, +) +from .user_management_multi_factor_authentication_order import ( + UserManagementMultiFactorAuthenticationOrder as UserManagementMultiFactorAuthenticationOrder, +) diff --git a/src/workos/user_management/multi_factor_authentication/models/enroll_user_authentication_factor.py b/src/workos/user_management/multi_factor_authentication/models/enroll_user_authentication_factor.py new file mode 100644 index 00000000..598f19fe --- /dev/null +++ b/src/workos/user_management/multi_factor_authentication/models/enroll_user_authentication_factor.py @@ -0,0 +1,48 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class EnrollUserAuthenticationFactor: + """Enroll User Authentication Factor model.""" + + type: Literal["totp"] + """The type of the factor to enroll.""" + totp_issuer: Optional[str] = None + """Your application or company name displayed in the user's authenticator app.""" + totp_user: Optional[str] = None + """The user's account name displayed in their authenticator app.""" + totp_secret: Optional[str] = None + """The Base32-encoded shared secret for TOTP factors. This can be provided when creating the auth factor, otherwise it will be generated. The algorithm used to derive TOTP codes is SHA-1, the code length is 6 digits, and the timestep is 30 seconds – the secret must be compatible with these parameters.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "EnrollUserAuthenticationFactor": + """Deserialize from a dictionary.""" + try: + return cls( + type=data["type"], + totp_issuer=data.get("totp_issuer"), + totp_user=data.get("totp_user"), + totp_secret=data.get("totp_secret"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing EnrollUserAuthenticationFactor: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["type"] = self.type + if self.totp_issuer is not None: + result["totp_issuer"] = self.totp_issuer + if self.totp_user is not None: + result["totp_user"] = self.totp_user + if self.totp_secret is not None: + result["totp_secret"] = self.totp_secret + return result diff --git a/src/workos/user_management/multi_factor_authentication/models/user_authentication_factor_enroll_response.py b/src/workos/user_management/multi_factor_authentication/models/user_authentication_factor_enroll_response.py new file mode 100644 index 00000000..c5bb74e3 --- /dev/null +++ b/src/workos/user_management/multi_factor_authentication/models/user_authentication_factor_enroll_response.py @@ -0,0 +1,46 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict +from workos._errors import BaseRequestException + +from workos.multi_factor_auth.challenges.models import AuthenticationChallenge +from workos.multi_factor_auth.models import AuthenticationFactorEnrolled + + +@dataclass(slots=True) +class UserAuthenticationFactorEnrollResponse: + """User Authentication Factor Enroll Response model.""" + + authentication_factor: "AuthenticationFactorEnrolled" + authentication_challenge: "AuthenticationChallenge" + """The [authentication challenge](https://workos.com/docs/reference/authkit/mfa/authentication-challenge) object that is used to complete the authentication process.""" + + @classmethod + def from_dict( + cls, data: Dict[str, Any] + ) -> "UserAuthenticationFactorEnrollResponse": + """Deserialize from a dictionary.""" + try: + return cls( + authentication_factor=AuthenticationFactorEnrolled.from_dict( + cast(Dict[str, Any], data["authentication_factor"]) + ), + authentication_challenge=AuthenticationChallenge.from_dict( + cast(Dict[str, Any], data["authentication_challenge"]) + ), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UserAuthenticationFactorEnrollResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["authentication_factor"] = self.authentication_factor.to_dict() + result["authentication_challenge"] = self.authentication_challenge.to_dict() + return result diff --git a/src/workos/user_management/multi_factor_authentication/models/user_management_multi_factor_authentication_order.py b/src/workos/user_management/multi_factor_authentication/models/user_management_multi_factor_authentication_order.py new file mode 100644 index 00000000..aebe4d67 --- /dev/null +++ b/src/workos/user_management/multi_factor_authentication/models/user_management_multi_factor_authentication_order.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import ApplicationsOrder + +UserManagementMultiFactorAuthenticationOrder = ApplicationsOrder +__all__ = ["UserManagementMultiFactorAuthenticationOrder"] diff --git a/src/workos/user_management/organization_membership/__init__.py b/src/workos/user_management/organization_membership/__init__.py new file mode 100644 index 00000000..b52a733d --- /dev/null +++ b/src/workos/user_management/organization_membership/__init__.py @@ -0,0 +1,7 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import ( + UserManagementOrganizationMembership, + AsyncUserManagementOrganizationMembership, +) +from .models import * diff --git a/src/workos/user_management/organization_membership/_resource.py b/src/workos/user_management/organization_membership/_resource.py new file mode 100644 index 00000000..52e015c2 --- /dev/null +++ b/src/workos/user_management/organization_membership/_resource.py @@ -0,0 +1,596 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +if TYPE_CHECKING: + from ..._client import AsyncWorkOSClient, WorkOSClient + +from .models import OrganizationMembership, UserOrganizationMembership +from .models import ( + UserManagementOrganizationMembershipOrder, + UserManagementOrganizationMembershipStatuses, +) +from ..._pagination import AsyncPage, SyncPage +from ..._types import RequestOptions + + +class UserManagementOrganizationMembership: + """User Management Organization Membership API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[UserManagementOrganizationMembershipOrder] = None, + organization_id: Optional[str] = None, + statuses: Optional[List[UserManagementOrganizationMembershipStatuses]] = None, + user_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[UserOrganizationMembership]: + """List organization memberships + + Get a list of all organization memberships matching the criteria specified. At least one of `user_id` or `organization_id` must be provided. By default only active memberships are returned. Use the `statuses` parameter to filter by other statuses. + + Args: + organization_id: The ID of the [organization](https://workos.com/docs/reference/organization) which the user belongs to. + statuses: Filter by the status of the organization membership. Array including any of `active`, `inactive`, or `pending`. + user_id: The ID of the [user](https://workos.com/docs/reference/authkit/user). + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[UserOrganizationMembership] + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "organization_id": organization_id, + "statuses": statuses, + "user_id": user_id, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path="user_management/organization_memberships", + model=UserOrganizationMembership, + params=params, + request_options=request_options, + ) + + def create( + self, + *, + user_id: str, + organization_id: str, + role_slug: Optional[str] = None, + role_slugs: Optional[List[str]] = None, + request_options: Optional[RequestOptions] = None, + ) -> OrganizationMembership: + """Create an organization membership + + Creates a new `active` organization membership for the given organization and user. + + Calling this API with an organization and user that match an `inactive` organization membership will activate the membership with the specified role(s). + + Args: + user_id: The ID of the [user](https://workos.com/docs/reference/authkit/user). + organization_id: The ID of the [organization](https://workos.com/docs/reference/organization) which the user belongs to. + role_slug: A single role identifier. Defaults to `member` or the explicit default role. Mutually exclusive with `role_slugs`. + role_slugs: An array of role identifiers. Limited to one role when Multiple Roles is disabled. Mutually exclusive with `role_slug`. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + OrganizationMembership + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "user_id": user_id, + "organization_id": organization_id, + "role_slug": role_slug, + "role_slugs": role_slugs, + }.items() + if v is not None + } + return self._client.request( + method="post", + path="user_management/organization_memberships", + body=body, + model=OrganizationMembership, + request_options=request_options, + ) + + def get( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> UserOrganizationMembership: + """Get an organization membership + + Get the details of an existing organization membership. + + Args: + id: The unique ID of the organization membership. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + UserOrganizationMembership + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"user_management/organization_memberships/{id}", + model=UserOrganizationMembership, + request_options=request_options, + ) + + def update( + self, + id: str, + *, + role_slug: Optional[str] = None, + role_slugs: Optional[List[str]] = None, + request_options: Optional[RequestOptions] = None, + ) -> UserOrganizationMembership: + """Update an organization membership + + Update the details of an existing organization membership. + + Args: + id: The unique ID of the organization membership. + role_slug: A single role identifier. Defaults to `member` or the explicit default role. Mutually exclusive with `role_slugs`. + role_slugs: An array of role identifiers. Limited to one role when Multiple Roles is disabled. Mutually exclusive with `role_slug`. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + UserOrganizationMembership + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "role_slug": role_slug, + "role_slugs": role_slugs, + }.items() + if v is not None + } + return self._client.request( + method="put", + path=f"user_management/organization_memberships/{id}", + body=body, + model=UserOrganizationMembership, + request_options=request_options, + ) + + def delete( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete an organization membership + + Permanently deletes an existing organization membership. It cannot be undone. + + Args: + id: The unique ID of the organization membership. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + self._client.request( + method="delete", + path=f"user_management/organization_memberships/{id}", + request_options=request_options, + ) + + def deactivate( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> OrganizationMembership: + """Deactivate an organization membership + + Deactivates an `active` organization membership. Emits an [organization_membership.updated](https://workos.com/docs/events/organization-membership) event upon successful deactivation. + + - Deactivating an `inactive` membership is a no-op and does not emit an event. + - Deactivating a `pending` membership returns an error. This membership should be [deleted](https://workos.com/docs/reference/authkit/organization-membership/delete) instead. + + See the [membership management documentation](https://workos.com/docs/authkit/users-organizations/organizations/membership-management) for additional details. + + Args: + id: The unique ID of the organization membership. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + OrganizationMembership + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="put", + path=f"user_management/organization_memberships/{id}/deactivate", + model=OrganizationMembership, + request_options=request_options, + ) + + def reactivate( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> UserOrganizationMembership: + """Reactivate an organization membership + + Reactivates an `inactive` organization membership, retaining the pre-existing role(s). Emits an [organization_membership.updated](https://workos.com/docs/events/organization-membership) event upon successful reactivation. + + - Reactivating an `active` membership is a no-op and does not emit an event. + - Reactivating a `pending` membership returns an error. The user needs to [accept the invitation](https://workos.com/docs/authkit/invitations) instead. + + See the [membership management documentation](https://workos.com/docs/authkit/users-organizations/organizations/membership-management) for additional details. + + Args: + id: The unique ID of the organization membership. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + UserOrganizationMembership + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="put", + path=f"user_management/organization_memberships/{id}/reactivate", + model=UserOrganizationMembership, + request_options=request_options, + ) + + +class AsyncUserManagementOrganizationMembership: + """User Management Organization Membership API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[UserManagementOrganizationMembershipOrder] = None, + organization_id: Optional[str] = None, + statuses: Optional[List[UserManagementOrganizationMembershipStatuses]] = None, + user_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[UserOrganizationMembership]: + """List organization memberships + + Get a list of all organization memberships matching the criteria specified. At least one of `user_id` or `organization_id` must be provided. By default only active memberships are returned. Use the `statuses` parameter to filter by other statuses. + + Args: + organization_id: The ID of the [organization](https://workos.com/docs/reference/organization) which the user belongs to. + statuses: Filter by the status of the organization membership. Array including any of `active`, `inactive`, or `pending`. + user_id: The ID of the [user](https://workos.com/docs/reference/authkit/user). + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[UserOrganizationMembership] + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "organization_id": organization_id, + "statuses": statuses, + "user_id": user_id, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path="user_management/organization_memberships", + model=UserOrganizationMembership, + params=params, + request_options=request_options, + ) + + async def create( + self, + *, + user_id: str, + organization_id: str, + role_slug: Optional[str] = None, + role_slugs: Optional[List[str]] = None, + request_options: Optional[RequestOptions] = None, + ) -> OrganizationMembership: + """Create an organization membership + + Creates a new `active` organization membership for the given organization and user. + + Calling this API with an organization and user that match an `inactive` organization membership will activate the membership with the specified role(s). + + Args: + user_id: The ID of the [user](https://workos.com/docs/reference/authkit/user). + organization_id: The ID of the [organization](https://workos.com/docs/reference/organization) which the user belongs to. + role_slug: A single role identifier. Defaults to `member` or the explicit default role. Mutually exclusive with `role_slugs`. + role_slugs: An array of role identifiers. Limited to one role when Multiple Roles is disabled. Mutually exclusive with `role_slug`. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + OrganizationMembership + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "user_id": user_id, + "organization_id": organization_id, + "role_slug": role_slug, + "role_slugs": role_slugs, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path="user_management/organization_memberships", + body=body, + model=OrganizationMembership, + request_options=request_options, + ) + + async def get( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> UserOrganizationMembership: + """Get an organization membership + + Get the details of an existing organization membership. + + Args: + id: The unique ID of the organization membership. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + UserOrganizationMembership + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"user_management/organization_memberships/{id}", + model=UserOrganizationMembership, + request_options=request_options, + ) + + async def update( + self, + id: str, + *, + role_slug: Optional[str] = None, + role_slugs: Optional[List[str]] = None, + request_options: Optional[RequestOptions] = None, + ) -> UserOrganizationMembership: + """Update an organization membership + + Update the details of an existing organization membership. + + Args: + id: The unique ID of the organization membership. + role_slug: A single role identifier. Defaults to `member` or the explicit default role. Mutually exclusive with `role_slugs`. + role_slugs: An array of role identifiers. Limited to one role when Multiple Roles is disabled. Mutually exclusive with `role_slug`. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + UserOrganizationMembership + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "role_slug": role_slug, + "role_slugs": role_slugs, + }.items() + if v is not None + } + return await self._client.request( + method="put", + path=f"user_management/organization_memberships/{id}", + body=body, + model=UserOrganizationMembership, + request_options=request_options, + ) + + async def delete( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete an organization membership + + Permanently deletes an existing organization membership. It cannot be undone. + + Args: + id: The unique ID of the organization membership. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + await self._client.request( + method="delete", + path=f"user_management/organization_memberships/{id}", + request_options=request_options, + ) + + async def deactivate( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> OrganizationMembership: + """Deactivate an organization membership + + Deactivates an `active` organization membership. Emits an [organization_membership.updated](https://workos.com/docs/events/organization-membership) event upon successful deactivation. + + - Deactivating an `inactive` membership is a no-op and does not emit an event. + - Deactivating a `pending` membership returns an error. This membership should be [deleted](https://workos.com/docs/reference/authkit/organization-membership/delete) instead. + + See the [membership management documentation](https://workos.com/docs/authkit/users-organizations/organizations/membership-management) for additional details. + + Args: + id: The unique ID of the organization membership. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + OrganizationMembership + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="put", + path=f"user_management/organization_memberships/{id}/deactivate", + model=OrganizationMembership, + request_options=request_options, + ) + + async def reactivate( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> UserOrganizationMembership: + """Reactivate an organization membership + + Reactivates an `inactive` organization membership, retaining the pre-existing role(s). Emits an [organization_membership.updated](https://workos.com/docs/events/organization-membership) event upon successful reactivation. + + - Reactivating an `active` membership is a no-op and does not emit an event. + - Reactivating a `pending` membership returns an error. The user needs to [accept the invitation](https://workos.com/docs/authkit/invitations) instead. + + See the [membership management documentation](https://workos.com/docs/authkit/users-organizations/organizations/membership-management) for additional details. + + Args: + id: The unique ID of the organization membership. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + UserOrganizationMembership + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="put", + path=f"user_management/organization_memberships/{id}/reactivate", + model=UserOrganizationMembership, + request_options=request_options, + ) diff --git a/src/workos/user_management/organization_membership/models/__init__.py b/src/workos/user_management/organization_membership/models/__init__.py new file mode 100644 index 00000000..6d359fd9 --- /dev/null +++ b/src/workos/user_management/organization_membership/models/__init__.py @@ -0,0 +1,18 @@ +# This file is auto-generated by oagen. Do not edit. + +from .create_user_organization_membership import ( + CreateUserOrganizationMembership as CreateUserOrganizationMembership, +) +from .organization_membership import OrganizationMembership as OrganizationMembership +from .update_user_organization_membership import ( + UpdateUserOrganizationMembership as UpdateUserOrganizationMembership, +) +from .user_management_organization_membership_order import ( + UserManagementOrganizationMembershipOrder as UserManagementOrganizationMembershipOrder, +) +from .user_management_organization_membership_statuses import ( + UserManagementOrganizationMembershipStatuses as UserManagementOrganizationMembershipStatuses, +) +from .user_organization_membership import ( + UserOrganizationMembership as UserOrganizationMembership, +) diff --git a/src/workos/user_management/organization_membership/models/create_user_organization_membership.py b/src/workos/user_management/organization_membership/models/create_user_organization_membership.py new file mode 100644 index 00000000..08fc474b --- /dev/null +++ b/src/workos/user_management/organization_membership/models/create_user_organization_membership.py @@ -0,0 +1,47 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, List, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class CreateUserOrganizationMembership: + """Create User Organization Membership model.""" + + user_id: str + """The ID of the [user](https://workos.com/docs/reference/authkit/user).""" + organization_id: str + """The ID of the [organization](https://workos.com/docs/reference/organization) which the user belongs to.""" + role_slug: Optional[str] = None + """A single role identifier. Defaults to `member` or the explicit default role. Mutually exclusive with `role_slugs`.""" + role_slugs: Optional[List[str]] = None + """An array of role identifiers. Limited to one role when Multiple Roles is disabled. Mutually exclusive with `role_slug`.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CreateUserOrganizationMembership": + """Deserialize from a dictionary.""" + try: + return cls( + user_id=data["user_id"], + organization_id=data["organization_id"], + role_slug=data.get("role_slug"), + role_slugs=data.get("role_slugs"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing CreateUserOrganizationMembership: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["user_id"] = self.user_id + result["organization_id"] = self.organization_id + if self.role_slug is not None: + result["role_slug"] = self.role_slug + if self.role_slugs is not None: + result["role_slugs"] = self.role_slugs + return result diff --git a/src/workos/user_management/organization_membership/models/organization_membership.py b/src/workos/user_management/organization_membership/models/organization_membership.py new file mode 100644 index 00000000..9d4f8ebc --- /dev/null +++ b/src/workos/user_management/organization_membership/models/organization_membership.py @@ -0,0 +1,88 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import cast +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + +from workos.authorization.models import SlimRole +from workos.common.models import OrganizationMembershipStatus + + +@dataclass(slots=True) +class OrganizationMembership: + """Organization Membership model.""" + + object: Literal["organization_membership"] + """Distinguishes the organization membership object.""" + id: str + """The unique ID of the organization membership.""" + user_id: str + """The ID of the user.""" + organization_id: str + """The ID of the organization which the user belongs to.""" + status: "OrganizationMembershipStatus" + """The status of the organization membership. One of `active`, `inactive`, or `pending`.""" + directory_managed: bool + """Whether this organization membership is managed by a directory sync connection.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + role: "SlimRole" + """The primary role assigned to the user within the organization.""" + organization_name: Optional[str] = None + """The name of the organization which the user belongs to.""" + custom_attributes: Optional[Dict[str, Any]] = None + """An object containing IdP-sourced attributes from the linked [Directory User](https://workos.com/docs/reference/directory-sync/directory-user) or [SSO Profile](https://workos.com/docs/reference/sso/profile). Directory User attributes take precedence when both are linked.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "OrganizationMembership": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + user_id=data["user_id"], + organization_id=data["organization_id"], + status=OrganizationMembershipStatus(data["status"]), + directory_managed=data["directory_managed"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + role=SlimRole.from_dict(cast(Dict[str, Any], data["role"])), + organization_name=data.get("organization_name"), + custom_attributes=data.get("custom_attributes"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing OrganizationMembership: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["user_id"] = self.user_id + result["organization_id"] = self.organization_id + result["status"] = self.status + result["directory_managed"] = self.directory_managed + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["role"] = self.role.to_dict() + if self.organization_name is not None: + result["organization_name"] = self.organization_name + if self.custom_attributes is not None: + result["custom_attributes"] = self.custom_attributes + return result diff --git a/src/workos/user_management/organization_membership/models/update_user_organization_membership.py b/src/workos/user_management/organization_membership/models/update_user_organization_membership.py new file mode 100644 index 00000000..1c78b0d6 --- /dev/null +++ b/src/workos/user_management/organization_membership/models/update_user_organization_membership.py @@ -0,0 +1,39 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, List, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class UpdateUserOrganizationMembership: + """Update User Organization Membership model.""" + + role_slug: Optional[str] = None + """A single role identifier. Defaults to `member` or the explicit default role. Mutually exclusive with `role_slugs`.""" + role_slugs: Optional[List[str]] = None + """An array of role identifiers. Limited to one role when Multiple Roles is disabled. Mutually exclusive with `role_slug`.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "UpdateUserOrganizationMembership": + """Deserialize from a dictionary.""" + try: + return cls( + role_slug=data.get("role_slug"), + role_slugs=data.get("role_slugs"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UpdateUserOrganizationMembership: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + if self.role_slug is not None: + result["role_slug"] = self.role_slug + if self.role_slugs is not None: + result["role_slugs"] = self.role_slugs + return result diff --git a/src/workos/user_management/organization_membership/models/user_management_organization_membership_order.py b/src/workos/user_management/organization_membership/models/user_management_organization_membership_order.py new file mode 100644 index 00000000..9dd1dba6 --- /dev/null +++ b/src/workos/user_management/organization_membership/models/user_management_organization_membership_order.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import ApplicationsOrder + +UserManagementOrganizationMembershipOrder = ApplicationsOrder +__all__ = ["UserManagementOrganizationMembershipOrder"] diff --git a/src/workos/user_management/organization_membership/models/user_management_organization_membership_statuses.py b/src/workos/user_management/organization_membership/models/user_management_organization_membership_statuses.py new file mode 100644 index 00000000..ea0e7209 --- /dev/null +++ b/src/workos/user_management/organization_membership/models/user_management_organization_membership_statuses.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.common.models import OrganizationMembershipStatus + +UserManagementOrganizationMembershipStatuses = OrganizationMembershipStatus +__all__ = ["UserManagementOrganizationMembershipStatuses"] diff --git a/src/workos/user_management/organization_membership/models/user_organization_membership.py b/src/workos/user_management/organization_membership/models/user_organization_membership.py new file mode 100644 index 00000000..81de3c6e --- /dev/null +++ b/src/workos/user_management/organization_membership/models/user_organization_membership.py @@ -0,0 +1,88 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import cast +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + +from workos.authorization.models import SlimRole +from workos.common.models import UserOrganizationMembershipStatus + + +@dataclass(slots=True) +class UserOrganizationMembership: + """User Organization Membership model.""" + + object: Literal["organization_membership"] + """Distinguishes the organization membership object.""" + id: str + """The unique ID of the organization membership.""" + user_id: str + """The ID of the user.""" + organization_id: str + """The ID of the organization which the user belongs to.""" + status: "UserOrganizationMembershipStatus" + """The status of the organization membership. One of `active`, `inactive`, or `pending`.""" + directory_managed: bool + """Whether this organization membership is managed by a directory sync connection.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + role: "SlimRole" + """The primary role assigned to the user within the organization.""" + organization_name: Optional[str] = None + """The name of the organization which the user belongs to.""" + custom_attributes: Optional[Dict[str, Any]] = None + """An object containing IdP-sourced attributes from the linked [Directory User](https://workos.com/docs/reference/directory-sync/directory-user) or [SSO Profile](https://workos.com/docs/reference/sso/profile). Directory User attributes take precedence when both are linked.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "UserOrganizationMembership": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + user_id=data["user_id"], + organization_id=data["organization_id"], + status=UserOrganizationMembershipStatus(data["status"]), + directory_managed=data["directory_managed"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + role=SlimRole.from_dict(cast(Dict[str, Any], data["role"])), + organization_name=data.get("organization_name"), + custom_attributes=data.get("custom_attributes"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UserOrganizationMembership: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["user_id"] = self.user_id + result["organization_id"] = self.organization_id + result["status"] = self.status + result["directory_managed"] = self.directory_managed + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["role"] = self.role.to_dict() + if self.organization_name is not None: + result["organization_name"] = self.organization_name + if self.custom_attributes is not None: + result["custom_attributes"] = self.custom_attributes + return result diff --git a/src/workos/user_management/redirect_uris/__init__.py b/src/workos/user_management/redirect_uris/__init__.py new file mode 100644 index 00000000..8ae0c970 --- /dev/null +++ b/src/workos/user_management/redirect_uris/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import UserManagementRedirectUris, AsyncUserManagementRedirectUris +from .models import * diff --git a/src/workos/user_management/redirect_uris/_resource.py b/src/workos/user_management/redirect_uris/_resource.py new file mode 100644 index 00000000..b6dcc7b3 --- /dev/null +++ b/src/workos/user_management/redirect_uris/_resource.py @@ -0,0 +1,95 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, Optional + +if TYPE_CHECKING: + from ..._client import AsyncWorkOSClient, WorkOSClient + +from .models import RedirectUri +from ..._types import RequestOptions + + +class UserManagementRedirectUris: + """User Management Redirect Uris API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def create( + self, + *, + uri: str, + request_options: Optional[RequestOptions] = None, + ) -> RedirectUri: + """Create a redirect URI + + Creates a new redirect URI for an environment. + + Args: + uri: The redirect URI to create. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + RedirectUri + + Raises: + BadRequestException: If the request is malformed (400). + AuthenticationException: If the API key is invalid (401). + UnprocessableEntityException: If the request data is unprocessable (422). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "uri": uri, + } + return self._client.request( + method="post", + path="user_management/redirect_uris", + body=body, + model=RedirectUri, + request_options=request_options, + ) + + +class AsyncUserManagementRedirectUris: + """User Management Redirect Uris API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def create( + self, + *, + uri: str, + request_options: Optional[RequestOptions] = None, + ) -> RedirectUri: + """Create a redirect URI + + Creates a new redirect URI for an environment. + + Args: + uri: The redirect URI to create. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + RedirectUri + + Raises: + BadRequestException: If the request is malformed (400). + AuthenticationException: If the API key is invalid (401). + UnprocessableEntityException: If the request data is unprocessable (422). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "uri": uri, + } + return await self._client.request( + method="post", + path="user_management/redirect_uris", + body=body, + model=RedirectUri, + request_options=request_options, + ) diff --git a/src/workos/user_management/redirect_uris/models/__init__.py b/src/workos/user_management/redirect_uris/models/__init__.py new file mode 100644 index 00000000..86e8fc55 --- /dev/null +++ b/src/workos/user_management/redirect_uris/models/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from .create_redirect_uri import CreateRedirectUri as CreateRedirectUri +from .redirect_uri import RedirectUri as RedirectUri diff --git a/src/workos/user_management/redirect_uris/models/create_redirect_uri.py b/src/workos/user_management/redirect_uris/models/create_redirect_uri.py new file mode 100644 index 00000000..9f366408 --- /dev/null +++ b/src/workos/user_management/redirect_uris/models/create_redirect_uri.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class CreateRedirectUri: + """Create Redirect Uri model.""" + + uri: str + """The redirect URI to create.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CreateRedirectUri": + """Deserialize from a dictionary.""" + try: + return cls( + uri=data["uri"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing CreateRedirectUri: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["uri"] = self.uri + return result diff --git a/src/workos/user_management/redirect_uris/models/redirect_uri.py b/src/workos/user_management/redirect_uris/models/redirect_uri.py new file mode 100644 index 00000000..b977b95c --- /dev/null +++ b/src/workos/user_management/redirect_uris/models/redirect_uri.py @@ -0,0 +1,53 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Literal +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class RedirectUri: + """Redirect Uri model.""" + + object: Literal["redirect_uri"] + """The object type.""" + id: str + """The ID of the redirect URI.""" + uri: str + """The redirect URI.""" + default: bool + """Whether this is the default redirect URI.""" + created_at: str + """The timestamp when the redirect URI was created.""" + updated_at: str + """The timestamp when the redirect URI was last updated.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "RedirectUri": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + uri=data["uri"], + default=data["default"], + created_at=data["created_at"], + updated_at=data["updated_at"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing RedirectUri: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["uri"] = self.uri + result["default"] = self.default + result["created_at"] = self.created_at + result["updated_at"] = self.updated_at + return result diff --git a/src/workos/user_management/session_tokens/__init__.py b/src/workos/user_management/session_tokens/__init__.py new file mode 100644 index 00000000..09a5f607 --- /dev/null +++ b/src/workos/user_management/session_tokens/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import UserManagementSessionTokens, AsyncUserManagementSessionTokens +from .models import * diff --git a/src/workos/user_management/session_tokens/_resource.py b/src/workos/user_management/session_tokens/_resource.py new file mode 100644 index 00000000..4dd36804 --- /dev/null +++ b/src/workos/user_management/session_tokens/_resource.py @@ -0,0 +1,85 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from ..._client import AsyncWorkOSClient, WorkOSClient + +from .models import JwksResponse +from ..._types import RequestOptions + + +class UserManagementSessionTokens: + """User Management Session Tokens API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def json_web_key_set( + self, + client_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> JwksResponse: + """Get JWKS + + Returns the JSON Web Key Set (JWKS) containing the public keys used for verifying access tokens. + + Args: + client_id: Identifies the application making the request to the WorkOS server. You can obtain your client ID from the [API Keys](https://dashboard.workos.com/api-keys) page in the dashboard. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + JwksResponse + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"sso/jwks/{client_id}", + model=JwksResponse, + request_options=request_options, + ) + + +class AsyncUserManagementSessionTokens: + """User Management Session Tokens API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def json_web_key_set( + self, + client_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> JwksResponse: + """Get JWKS + + Returns the JSON Web Key Set (JWKS) containing the public keys used for verifying access tokens. + + Args: + client_id: Identifies the application making the request to the WorkOS server. You can obtain your client ID from the [API Keys](https://dashboard.workos.com/api-keys) page in the dashboard. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + JwksResponse + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"sso/jwks/{client_id}", + model=JwksResponse, + request_options=request_options, + ) diff --git a/src/workos/user_management/session_tokens/models/__init__.py b/src/workos/user_management/session_tokens/models/__init__.py new file mode 100644 index 00000000..d5284b9a --- /dev/null +++ b/src/workos/user_management/session_tokens/models/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from .jwks_response import JwksResponse as JwksResponse +from .jwks_response_keys import JwksResponseKeys as JwksResponseKeys diff --git a/src/workos/user_management/session_tokens/models/jwks_response.py b/src/workos/user_management/session_tokens/models/jwks_response.py new file mode 100644 index 00000000..4b6d5117 --- /dev/null +++ b/src/workos/user_management/session_tokens/models/jwks_response.py @@ -0,0 +1,39 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, List +from workos._errors import BaseRequestException + +from .jwks_response_keys import JwksResponseKeys + + +@dataclass(slots=True) +class JwksResponse: + """Jwks Response model.""" + + keys: List["JwksResponseKeys"] + """The public keys used for verifying access tokens.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "JwksResponse": + """Deserialize from a dictionary.""" + try: + return cls( + keys=[ + JwksResponseKeys.from_dict(cast(Dict[str, Any], item)) + for item in cast(list[Any], data["keys"]) + ], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing JwksResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["keys"] = [item.to_dict() for item in self.keys] + return result diff --git a/src/workos/user_management/session_tokens/models/jwks_response_keys.py b/src/workos/user_management/session_tokens/models/jwks_response_keys.py new file mode 100644 index 00000000..b99d4721 --- /dev/null +++ b/src/workos/user_management/session_tokens/models/jwks_response_keys.py @@ -0,0 +1,61 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, List, Literal +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class JwksResponseKeys: + """Jwks Response Keys model.""" + + alg: Literal["RS256"] + """Algorithm.""" + kty: Literal["RSA"] + """Key type.""" + use: Literal["sig"] + """Key use (signature).""" + x_5_c: List[str] + """X.509 certificate chain.""" + n: str + """RSA modulus.""" + e: str + """RSA exponent.""" + kid: str + """Key ID.""" + x_5_t_s_256: str + """X.509 certificate SHA-256 thumbprint.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "JwksResponseKeys": + """Deserialize from a dictionary.""" + try: + return cls( + alg=data["alg"], + kty=data["kty"], + use=data["use"], + x_5_c=data["x5c"], + n=data["n"], + e=data["e"], + kid=data["kid"], + x_5_t_s_256=data["x5t#S256"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing JwksResponseKeys: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["alg"] = self.alg + result["kty"] = self.kty + result["use"] = self.use + result["x5c"] = self.x_5_c + result["n"] = self.n + result["e"] = self.e + result["kid"] = self.kid + result["x5t#S256"] = self.x_5_t_s_256 + return result diff --git a/src/workos/user_management/users/__init__.py b/src/workos/user_management/users/__init__.py new file mode 100644 index 00000000..048dcd54 --- /dev/null +++ b/src/workos/user_management/users/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import UserManagementUsers, AsyncUserManagementUsers +from .models import * diff --git a/src/workos/user_management/users/_resource.py b/src/workos/user_management/users/_resource.py new file mode 100644 index 00000000..c5e2c610 --- /dev/null +++ b/src/workos/user_management/users/_resource.py @@ -0,0 +1,1172 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast + +if TYPE_CHECKING: + from ..._client import AsyncWorkOSClient, WorkOSClient + +from .models import ( + EmailVerification, + PasswordReset, + ResetPasswordResponse, + SendVerificationEmailResponse, + UserIdentitiesGetItem, + UserSessionsListItem, + VerifyEmailResponse, +) +from workos.user_management.authentication.models import User +from .models import UserManagementUsersOrder +from workos.common.models import ( + CreateUserDtoPasswordHashType, + UpdateUserDtoPasswordHashType, +) +from ..._pagination import AsyncPage, SyncPage +from ..._types import RequestOptions + + +class UserManagementUsers: + """User Management Users API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def get_email_verification( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> EmailVerification: + """Get an email verification code + + Get the details of an existing email verification code that can be used to send an email to a user for verification. + + Args: + id: The ID of the email verification code. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + EmailVerification + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"user_management/email_verification/{id}", + model=EmailVerification, + request_options=request_options, + ) + + def create_password_reset_token( + self, + *, + email: str, + request_options: Optional[RequestOptions] = None, + ) -> PasswordReset: + """Create a password reset token + + Creates a one-time token that can be used to reset a user's password. + + Args: + email: The email address of the user requesting a password reset. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + PasswordReset + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + RateLimitExceededException: If rate limited (429). + AuthenticationException: If the API key is invalid (401). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "email": email, + } + return self._client.request( + method="post", + path="user_management/password_reset", + body=body, + model=PasswordReset, + request_options=request_options, + ) + + def reset_password( + self, + *, + token: str, + new_password: str, + request_options: Optional[RequestOptions] = None, + ) -> ResetPasswordResponse: + """Reset the password + + Sets a new password using the `token` query parameter from the link that the user received. Successfully resetting the password will verify a user's email, if it hasn't been verified yet. + + Args: + token: The password reset token. + new_password: The new password to set for the user. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + ResetPasswordResponse + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "token": token, + "new_password": new_password, + } + return self._client.request( + method="post", + path="user_management/password_reset/confirm", + body=body, + model=ResetPasswordResponse, + request_options=request_options, + ) + + def get_password_reset( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> PasswordReset: + """Get a password reset token + + Get the details of an existing password reset token that can be used to reset a user's password. + + Args: + id: The ID of the password reset token. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + PasswordReset + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"user_management/password_reset/{id}", + model=PasswordReset, + request_options=request_options, + ) + + def list_users( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[UserManagementUsersOrder] = None, + organization: Optional[str] = None, + organization_id: Optional[str] = None, + email: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[User]: + """List users + + Get a list of all of your existing users matching the criteria specified. + + Args: + organization: Filter users by the organization they are a member of. Deprecated in favor of `organization_id`. + organization_id: Filter users by the organization they are a member of. + email: Filter users by their email address. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[User] + + Raises: + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "organization": organization, + "organization_id": organization_id, + "email": email, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path="user_management/users", + model=User, + params=params, + request_options=request_options, + ) + + def create( + self, + *, + email: str, + password: Optional[str] = None, + password_hash: Optional[str] = None, + password_hash_type: Optional[CreateUserDtoPasswordHashType] = None, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + email_verified: Optional[bool] = None, + metadata: Optional[Dict[str, str]] = None, + external_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> User: + """Create a user + + Create a new user in the current environment. + + Args: + email: The email address of the user. + password: The password to set for the user. Mutually exclusive with `password_hash` and `password_hash_type`. + password_hash: The hashed password to set for the user. Mutually exclusive with `password`. + password_hash_type: The algorithm originally used to hash the password, used when providing a `password_hash`. + first_name: The first name of the user. + last_name: The last name of the user. + email_verified: Whether the user's email has been verified. + metadata: Object containing metadata key/value pairs associated with the user. + external_id: The external ID of the user. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + User + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "email": email, + "password": password, + "password_hash": password_hash, + "password_hash_type": password_hash_type, + "first_name": first_name, + "last_name": last_name, + "email_verified": email_verified, + "metadata": metadata, + "external_id": external_id, + }.items() + if v is not None + } + return self._client.request( + method="post", + path="user_management/users", + body=body, + model=User, + request_options=request_options, + ) + + def get_by_external_id( + self, + external_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> User: + """Get a user by external ID + + Get the details of an existing user by an [external identifier](https://workos.com/docs/authkit/metadata/external-identifiers). + + Args: + external_id: The external ID of the user. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + User + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"user_management/users/external_id/{external_id}", + model=User, + request_options=request_options, + ) + + def get_user( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> User: + """Get a user + + Get the details of an existing user. + + Args: + id: The unique ID of the user. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + User + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="get", + path=f"user_management/users/{id}", + model=User, + request_options=request_options, + ) + + get_users = get_user + + def update( + self, + id: str, + *, + email: Optional[str] = None, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + email_verified: Optional[bool] = None, + password: Optional[str] = None, + password_hash: Optional[str] = None, + password_hash_type: Optional[UpdateUserDtoPasswordHashType] = None, + metadata: Optional[Dict[str, str]] = None, + external_id: Optional[str] = None, + locale: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> User: + """Update a user + + Updates properties of a user. The omitted properties will be left unchanged. + + Args: + id: The unique ID of the user. + email: The email address of the user. + first_name: The first name of the user. + last_name: The last name of the user. + email_verified: Whether the user's email has been verified. + password: The password to set for the user. + password_hash: The hashed password to set for the user. Mutually exclusive with `password`. + password_hash_type: The algorithm originally used to hash the password, used when providing a `password_hash`. + metadata: Object containing metadata key/value pairs associated with the user. + external_id: The external ID of the user. + locale: The user's preferred locale. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + User + + Raises: + BadRequestException: If the request is malformed (400). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "email": email, + "first_name": first_name, + "last_name": last_name, + "email_verified": email_verified, + "password": password, + "password_hash": password_hash, + "password_hash_type": password_hash_type, + "metadata": metadata, + "external_id": external_id, + "locale": locale, + }.items() + if v is not None + } + return self._client.request( + method="put", + path=f"user_management/users/{id}", + body=body, + model=User, + request_options=request_options, + ) + + def delete( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a user + + Permanently deletes a user in the current environment. It cannot be undone. + + Args: + id: The unique ID of the user. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + self._client.request( + method="delete", + path=f"user_management/users/{id}", + request_options=request_options, + ) + + def email_verification( + self, + id: str, + *, + code: str, + request_options: Optional[RequestOptions] = None, + ) -> VerifyEmailResponse: + """Verify email + + Verifies an email address using the one-time code received by the user. + + Args: + id: The ID of the user. + code: The one-time email verification code. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + VerifyEmailResponse + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "code": code, + } + return self._client.request( + method="post", + path=f"user_management/users/{id}/email_verification/confirm", + body=body, + model=VerifyEmailResponse, + request_options=request_options, + ) + + def send_verification_email( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> SendVerificationEmailResponse: + """Send verification email + + Sends an email that contains a one-time code used to verify a user’s email address. + + Args: + id: The ID of the user. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SendVerificationEmailResponse + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + RateLimitExceededException: If rate limited (429). + AuthenticationException: If the API key is invalid (401). + ServerException: If the server returns a 5xx error. + """ + return self._client.request( + method="post", + path=f"user_management/users/{id}/email_verification/send", + model=SendVerificationEmailResponse, + request_options=request_options, + ) + + def get_identity( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> List[UserIdentitiesGetItem]: + """Get user identities + + Get a list of identities associated with the user. A user can have multiple associated identities after going through [identity linking](https://workos.com/docs/authkit/identity-linking). Currently only OAuth identities are supported. More provider types may be added in the future. + + Args: + id: The unique ID of the user. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + List[UserIdentitiesGetItem] + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + raw = self._client.request( + method="get", + path=f"user_management/users/{id}/identities", + request_options=request_options, + ) + return [ + UserIdentitiesGetItem.from_dict(cast(Dict[str, Any], item)) + for item in (raw if isinstance(raw, list) else []) + ] + + get_identities = get_identity + + def list_sessions( + self, + id: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[UserManagementUsersOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[UserSessionsListItem]: + """List sessions + + Get a list of all active sessions for a specific user. + + Args: + id: The ID of the user. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[UserSessionsListItem] + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path=f"user_management/users/{id}/sessions", + model=UserSessionsListItem, + params=params, + request_options=request_options, + ) + + +class AsyncUserManagementUsers: + """User Management Users API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def get_email_verification( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> EmailVerification: + """Get an email verification code + + Get the details of an existing email verification code that can be used to send an email to a user for verification. + + Args: + id: The ID of the email verification code. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + EmailVerification + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"user_management/email_verification/{id}", + model=EmailVerification, + request_options=request_options, + ) + + async def create_password_reset_token( + self, + *, + email: str, + request_options: Optional[RequestOptions] = None, + ) -> PasswordReset: + """Create a password reset token + + Creates a one-time token that can be used to reset a user's password. + + Args: + email: The email address of the user requesting a password reset. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + PasswordReset + + Raises: + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + RateLimitExceededException: If rate limited (429). + AuthenticationException: If the API key is invalid (401). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "email": email, + } + return await self._client.request( + method="post", + path="user_management/password_reset", + body=body, + model=PasswordReset, + request_options=request_options, + ) + + async def reset_password( + self, + *, + token: str, + new_password: str, + request_options: Optional[RequestOptions] = None, + ) -> ResetPasswordResponse: + """Reset the password + + Sets a new password using the `token` query parameter from the link that the user received. Successfully resetting the password will verify a user's email, if it hasn't been verified yet. + + Args: + token: The password reset token. + new_password: The new password to set for the user. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + ResetPasswordResponse + + Raises: + BadRequestException: If the request is malformed (400). + AuthorizationException: If the request is forbidden (403). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "token": token, + "new_password": new_password, + } + return await self._client.request( + method="post", + path="user_management/password_reset/confirm", + body=body, + model=ResetPasswordResponse, + request_options=request_options, + ) + + async def get_password_reset( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> PasswordReset: + """Get a password reset token + + Get the details of an existing password reset token that can be used to reset a user's password. + + Args: + id: The ID of the password reset token. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + PasswordReset + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"user_management/password_reset/{id}", + model=PasswordReset, + request_options=request_options, + ) + + async def list_users( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[UserManagementUsersOrder] = None, + organization: Optional[str] = None, + organization_id: Optional[str] = None, + email: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[User]: + """List users + + Get a list of all of your existing users matching the criteria specified. + + Args: + organization: Filter users by the organization they are a member of. Deprecated in favor of `organization_id`. + organization_id: Filter users by the organization they are a member of. + email: Filter users by their email address. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[User] + + Raises: + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + "organization": organization, + "organization_id": organization_id, + "email": email, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path="user_management/users", + model=User, + params=params, + request_options=request_options, + ) + + async def create( + self, + *, + email: str, + password: Optional[str] = None, + password_hash: Optional[str] = None, + password_hash_type: Optional[CreateUserDtoPasswordHashType] = None, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + email_verified: Optional[bool] = None, + metadata: Optional[Dict[str, str]] = None, + external_id: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> User: + """Create a user + + Create a new user in the current environment. + + Args: + email: The email address of the user. + password: The password to set for the user. Mutually exclusive with `password_hash` and `password_hash_type`. + password_hash: The hashed password to set for the user. Mutually exclusive with `password`. + password_hash_type: The algorithm originally used to hash the password, used when providing a `password_hash`. + first_name: The first name of the user. + last_name: The last name of the user. + email_verified: Whether the user's email has been verified. + metadata: Object containing metadata key/value pairs associated with the user. + external_id: The external ID of the user. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + User + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "email": email, + "password": password, + "password_hash": password_hash, + "password_hash_type": password_hash_type, + "first_name": first_name, + "last_name": last_name, + "email_verified": email_verified, + "metadata": metadata, + "external_id": external_id, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path="user_management/users", + body=body, + model=User, + request_options=request_options, + ) + + async def get_by_external_id( + self, + external_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> User: + """Get a user by external ID + + Get the details of an existing user by an [external identifier](https://workos.com/docs/authkit/metadata/external-identifiers). + + Args: + external_id: The external ID of the user. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + User + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"user_management/users/external_id/{external_id}", + model=User, + request_options=request_options, + ) + + async def get_user( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> User: + """Get a user + + Get the details of an existing user. + + Args: + id: The unique ID of the user. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + User + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="get", + path=f"user_management/users/{id}", + model=User, + request_options=request_options, + ) + + get_users = get_user + + async def update( + self, + id: str, + *, + email: Optional[str] = None, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + email_verified: Optional[bool] = None, + password: Optional[str] = None, + password_hash: Optional[str] = None, + password_hash_type: Optional[UpdateUserDtoPasswordHashType] = None, + metadata: Optional[Dict[str, str]] = None, + external_id: Optional[str] = None, + locale: Optional[str] = None, + request_options: Optional[RequestOptions] = None, + ) -> User: + """Update a user + + Updates properties of a user. The omitted properties will be left unchanged. + + Args: + id: The unique ID of the user. + email: The email address of the user. + first_name: The first name of the user. + last_name: The last name of the user. + email_verified: Whether the user's email has been verified. + password: The password to set for the user. + password_hash: The hashed password to set for the user. Mutually exclusive with `password`. + password_hash_type: The algorithm originally used to hash the password, used when providing a `password_hash`. + metadata: Object containing metadata key/value pairs associated with the user. + external_id: The external ID of the user. + locale: The user's preferred locale. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + User + + Raises: + BadRequestException: If the request is malformed (400). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "email": email, + "first_name": first_name, + "last_name": last_name, + "email_verified": email_verified, + "password": password, + "password_hash": password_hash, + "password_hash_type": password_hash_type, + "metadata": metadata, + "external_id": external_id, + "locale": locale, + }.items() + if v is not None + } + return await self._client.request( + method="put", + path=f"user_management/users/{id}", + body=body, + model=User, + request_options=request_options, + ) + + async def delete( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a user + + Permanently deletes a user in the current environment. It cannot be undone. + + Args: + id: The unique ID of the user. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + await self._client.request( + method="delete", + path=f"user_management/users/{id}", + request_options=request_options, + ) + + async def email_verification( + self, + id: str, + *, + code: str, + request_options: Optional[RequestOptions] = None, + ) -> VerifyEmailResponse: + """Verify email + + Verifies an email address using the one-time code received by the user. + + Args: + id: The ID of the user. + code: The one-time email verification code. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + VerifyEmailResponse + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "code": code, + } + return await self._client.request( + method="post", + path=f"user_management/users/{id}/email_verification/confirm", + body=body, + model=VerifyEmailResponse, + request_options=request_options, + ) + + async def send_verification_email( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> SendVerificationEmailResponse: + """Send verification email + + Sends an email that contains a one-time code used to verify a user’s email address. + + Args: + id: The ID of the user. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SendVerificationEmailResponse + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + RateLimitExceededException: If rate limited (429). + AuthenticationException: If the API key is invalid (401). + ServerException: If the server returns a 5xx error. + """ + return await self._client.request( + method="post", + path=f"user_management/users/{id}/email_verification/send", + model=SendVerificationEmailResponse, + request_options=request_options, + ) + + async def get_identity( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> List[UserIdentitiesGetItem]: + """Get user identities + + Get a list of identities associated with the user. A user can have multiple associated identities after going through [identity linking](https://workos.com/docs/authkit/identity-linking). Currently only OAuth identities are supported. More provider types may be added in the future. + + Args: + id: The unique ID of the user. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + List[UserIdentitiesGetItem] + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + raw = await self._client.request( + method="get", + path=f"user_management/users/{id}/identities", + request_options=request_options, + ) + return [ + UserIdentitiesGetItem.from_dict(cast(Dict[str, Any], item)) + for item in (raw if isinstance(raw, list) else []) + ] + + get_identities = get_identity + + async def list_sessions( + self, + id: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[UserManagementUsersOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[UserSessionsListItem]: + """List sessions + + Get a list of all active sessions for a specific user. + + Args: + id: The ID of the user. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[UserSessionsListItem] + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path=f"user_management/users/{id}/sessions", + model=UserSessionsListItem, + params=params, + request_options=request_options, + ) diff --git a/src/workos/user_management/users/models/__init__.py b/src/workos/user_management/users/models/__init__.py new file mode 100644 index 00000000..9c17db3a --- /dev/null +++ b/src/workos/user_management/users/models/__init__.py @@ -0,0 +1,24 @@ +# This file is auto-generated by oagen. Do not edit. + +from .create_password_reset import CreatePasswordReset as CreatePasswordReset +from .create_password_reset_token import ( + CreatePasswordResetToken as CreatePasswordResetToken, +) +from .create_user import CreateUser as CreateUser +from .email_verification import EmailVerification as EmailVerification +from .password_reset import PasswordReset as PasswordReset +from .reset_password_response import ResetPasswordResponse as ResetPasswordResponse +from .send_verification_email_response import ( + SendVerificationEmailResponse as SendVerificationEmailResponse, +) +from .update_user import UpdateUser as UpdateUser +from .user_identities_get_item import UserIdentitiesGetItem as UserIdentitiesGetItem +from .user_management_users_order import ( + UserManagementUsersOrder as UserManagementUsersOrder, +) +from .user_sessions_impersonator import ( + UserSessionsImpersonator as UserSessionsImpersonator, +) +from .user_sessions_list_item import UserSessionsListItem as UserSessionsListItem +from .verify_email_address import VerifyEmailAddress as VerifyEmailAddress +from .verify_email_response import VerifyEmailResponse as VerifyEmailResponse diff --git a/src/workos/user_management/users/models/create_password_reset.py b/src/workos/user_management/users/models/create_password_reset.py new file mode 100644 index 00000000..d5bca6f9 --- /dev/null +++ b/src/workos/user_management/users/models/create_password_reset.py @@ -0,0 +1,37 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class CreatePasswordReset: + """Create Password Reset model.""" + + token: str + """The password reset token.""" + new_password: str + """The new password to set for the user.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CreatePasswordReset": + """Deserialize from a dictionary.""" + try: + return cls( + token=data["token"], + new_password=data["new_password"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing CreatePasswordReset: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["token"] = self.token + result["new_password"] = self.new_password + return result diff --git a/src/workos/user_management/users/models/create_password_reset_token.py b/src/workos/user_management/users/models/create_password_reset_token.py new file mode 100644 index 00000000..218083b6 --- /dev/null +++ b/src/workos/user_management/users/models/create_password_reset_token.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class CreatePasswordResetToken: + """Create Password Reset Token model.""" + + email: str + """The email address of the user requesting a password reset.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CreatePasswordResetToken": + """Deserialize from a dictionary.""" + try: + return cls( + email=data["email"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing CreatePasswordResetToken: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["email"] = self.email + return result diff --git a/src/workos/user_management/users/models/create_user.py b/src/workos/user_management/users/models/create_user.py new file mode 100644 index 00000000..8c2c1398 --- /dev/null +++ b/src/workos/user_management/users/models/create_user.py @@ -0,0 +1,88 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException +from workos.common.models import CreateUserDtoPasswordHashType + + +@dataclass(slots=True) +class CreateUser: + """Create User model.""" + + email: str + """The email address of the user.""" + password: Optional[str] = None + """The password to set for the user. Mutually exclusive with `password_hash` and `password_hash_type`.""" + password_hash: Optional[str] = None + """The hashed password to set for the user. Mutually exclusive with `password`.""" + password_hash_type: Optional["CreateUserDtoPasswordHashType"] = None + """The algorithm originally used to hash the password, used when providing a `password_hash`.""" + first_name: Optional[str] = None + """The first name of the user.""" + last_name: Optional[str] = None + """The last name of the user.""" + email_verified: Optional[bool] = None + """Whether the user's email has been verified.""" + metadata: Optional[Dict[str, str]] = None + """Object containing metadata key/value pairs associated with the user.""" + external_id: Optional[str] = None + """The external ID of the user.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CreateUser": + """Deserialize from a dictionary.""" + try: + return cls( + email=data["email"], + password=data.get("password"), + password_hash=data.get("password_hash"), + password_hash_type=CreateUserDtoPasswordHashType(_v) + if (_v := data.get("password_hash_type")) is not None + else None, + first_name=data.get("first_name"), + last_name=data.get("last_name"), + email_verified=data.get("email_verified"), + metadata=data.get("metadata"), + external_id=data.get("external_id"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing CreateUser: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["email"] = self.email + if self.password is not None: + result["password"] = self.password + else: + result["password"] = None + if self.password_hash is not None: + result["password_hash"] = self.password_hash + if self.password_hash_type is not None: + result["password_hash_type"] = self.password_hash_type + if self.first_name is not None: + result["first_name"] = self.first_name + else: + result["first_name"] = None + if self.last_name is not None: + result["last_name"] = self.last_name + else: + result["last_name"] = None + if self.email_verified is not None: + result["email_verified"] = self.email_verified + else: + result["email_verified"] = None + if self.metadata is not None: + result["metadata"] = self.metadata + else: + result["metadata"] = None + if self.external_id is not None: + result["external_id"] = self.external_id + else: + result["external_id"] = None + return result diff --git a/src/workos/user_management/users/models/email_verification.py b/src/workos/user_management/users/models/email_verification.py new file mode 100644 index 00000000..33fe7ca5 --- /dev/null +++ b/src/workos/user_management/users/models/email_verification.py @@ -0,0 +1,74 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Literal +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class EmailVerification: + """Email Verification model.""" + + object: Literal["email_verification"] + """Distinguishes the email verification object.""" + id: str + """The unique ID of the email verification code.""" + user_id: str + """The unique ID of the user.""" + email: str + """The email address of the user.""" + expires_at: datetime + """The timestamp when the email verification code expires.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + code: str + """The code used to verify the email.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "EmailVerification": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + user_id=data["user_id"], + email=data["email"], + expires_at=datetime.fromisoformat( + data["expires_at"].replace("Z", "+00:00") + ), + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + code=data["code"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing EmailVerification: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["user_id"] = self.user_id + result["email"] = self.email + result["expires_at"] = self.expires_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["code"] = self.code + return result diff --git a/src/workos/user_management/users/models/password_reset.py b/src/workos/user_management/users/models/password_reset.py new file mode 100644 index 00000000..50051138 --- /dev/null +++ b/src/workos/user_management/users/models/password_reset.py @@ -0,0 +1,70 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Literal +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class PasswordReset: + """Password Reset model.""" + + object: Literal["password_reset"] + """Distinguishes the password reset object.""" + id: str + """The unique ID of the password reset object.""" + user_id: str + """The unique ID of the user.""" + email: str + """The email address of the user.""" + expires_at: datetime + """The timestamp when the password reset token expires.""" + created_at: datetime + """The timestamp when the password reset token was created.""" + password_reset_token: str + """The token used to reset the password.""" + password_reset_url: str + """The URL where the user can reset their password.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "PasswordReset": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + user_id=data["user_id"], + email=data["email"], + expires_at=datetime.fromisoformat( + data["expires_at"].replace("Z", "+00:00") + ), + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + password_reset_token=data["password_reset_token"], + password_reset_url=data["password_reset_url"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing PasswordReset: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["user_id"] = self.user_id + result["email"] = self.email + result["expires_at"] = self.expires_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["password_reset_token"] = self.password_reset_token + result["password_reset_url"] = self.password_reset_url + return result diff --git a/src/workos/user_management/users/models/reset_password_response.py b/src/workos/user_management/users/models/reset_password_response.py new file mode 100644 index 00000000..0145f4d0 --- /dev/null +++ b/src/workos/user_management/users/models/reset_password_response.py @@ -0,0 +1,36 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict +from workos._errors import BaseRequestException + +from workos.user_management.authentication.models import User + + +@dataclass(slots=True) +class ResetPasswordResponse: + """Reset Password Response model.""" + + user: "User" + """The user whose password was reset.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ResetPasswordResponse": + """Deserialize from a dictionary.""" + try: + return cls( + user=User.from_dict(cast(Dict[str, Any], data["user"])), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing ResetPasswordResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["user"] = self.user.to_dict() + return result diff --git a/src/workos/user_management/users/models/send_verification_email_response.py b/src/workos/user_management/users/models/send_verification_email_response.py new file mode 100644 index 00000000..9c66ebf4 --- /dev/null +++ b/src/workos/user_management/users/models/send_verification_email_response.py @@ -0,0 +1,36 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict +from workos._errors import BaseRequestException + +from workos.user_management.authentication.models import User + + +@dataclass(slots=True) +class SendVerificationEmailResponse: + """Send Verification Email Response model.""" + + user: "User" + """The user to whom the verification email was sent.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "SendVerificationEmailResponse": + """Deserialize from a dictionary.""" + try: + return cls( + user=User.from_dict(cast(Dict[str, Any], data["user"])), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing SendVerificationEmailResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["user"] = self.user.to_dict() + return result diff --git a/src/workos/user_management/users/models/update_user.py b/src/workos/user_management/users/models/update_user.py new file mode 100644 index 00000000..3f25da5b --- /dev/null +++ b/src/workos/user_management/users/models/update_user.py @@ -0,0 +1,88 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException +from workos.common.models import UpdateUserDtoPasswordHashType + + +@dataclass(slots=True) +class UpdateUser: + """Update User model.""" + + email: Optional[str] = None + """The email address of the user.""" + first_name: Optional[str] = None + """The first name of the user.""" + last_name: Optional[str] = None + """The last name of the user.""" + email_verified: Optional[bool] = None + """Whether the user's email has been verified.""" + password: Optional[str] = None + """The password to set for the user.""" + password_hash: Optional[str] = None + """The hashed password to set for the user. Mutually exclusive with `password`.""" + password_hash_type: Optional["UpdateUserDtoPasswordHashType"] = None + """The algorithm originally used to hash the password, used when providing a `password_hash`.""" + metadata: Optional[Dict[str, str]] = None + """Object containing metadata key/value pairs associated with the user.""" + external_id: Optional[str] = None + """The external ID of the user.""" + locale: Optional[str] = None + """The user's preferred locale.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "UpdateUser": + """Deserialize from a dictionary.""" + try: + return cls( + email=data.get("email"), + first_name=data.get("first_name"), + last_name=data.get("last_name"), + email_verified=data.get("email_verified"), + password=data.get("password"), + password_hash=data.get("password_hash"), + password_hash_type=UpdateUserDtoPasswordHashType(_v) + if (_v := data.get("password_hash_type")) is not None + else None, + metadata=data.get("metadata"), + external_id=data.get("external_id"), + locale=data.get("locale"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UpdateUser: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + if self.email is not None: + result["email"] = self.email + if self.first_name is not None: + result["first_name"] = self.first_name + if self.last_name is not None: + result["last_name"] = self.last_name + if self.email_verified is not None: + result["email_verified"] = self.email_verified + if self.password is not None: + result["password"] = self.password + if self.password_hash is not None: + result["password_hash"] = self.password_hash + if self.password_hash_type is not None: + result["password_hash_type"] = self.password_hash_type + if self.metadata is not None: + result["metadata"] = self.metadata + else: + result["metadata"] = None + if self.external_id is not None: + result["external_id"] = self.external_id + else: + result["external_id"] = None + if self.locale is not None: + result["locale"] = self.locale + else: + result["locale"] = None + return result diff --git a/src/workos/user_management/users/models/user_identities_get_item.py b/src/workos/user_management/users/models/user_identities_get_item.py new file mode 100644 index 00000000..13c6bccc --- /dev/null +++ b/src/workos/user_management/users/models/user_identities_get_item.py @@ -0,0 +1,42 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Literal +from workos._errors import BaseRequestException +from workos.common.models import UserIdentitiesGetItemProvider + + +@dataclass(slots=True) +class UserIdentitiesGetItem: + """User Identities Get Item model.""" + + idp_id: str + """The unique ID of the user in the external identity provider.""" + type: Literal["OAuth"] + """The type of the identity.""" + provider: "UserIdentitiesGetItemProvider" + """The type of OAuth provider for the identity.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "UserIdentitiesGetItem": + """Deserialize from a dictionary.""" + try: + return cls( + idp_id=data["idp_id"], + type=data["type"], + provider=UserIdentitiesGetItemProvider(data["provider"]), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UserIdentitiesGetItem: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["idp_id"] = self.idp_id + result["type"] = self.type + result["provider"] = self.provider + return result diff --git a/src/workos/user_management/users/models/user_management_users_order.py b/src/workos/user_management/users/models/user_management_users_order.py new file mode 100644 index 00000000..1305a4d5 --- /dev/null +++ b/src/workos/user_management/users/models/user_management_users_order.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import ApplicationsOrder + +UserManagementUsersOrder = ApplicationsOrder +__all__ = ["UserManagementUsersOrder"] diff --git a/src/workos/user_management/users/models/user_sessions_impersonator.py b/src/workos/user_management/users/models/user_sessions_impersonator.py new file mode 100644 index 00000000..3bac73a6 --- /dev/null +++ b/src/workos/user_management/users/models/user_sessions_impersonator.py @@ -0,0 +1,40 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class UserSessionsImpersonator: + """Information about the impersonator if this session was created via impersonation.""" + + email: str + """The email address of the WorkOS Dashboard user who is impersonating the user.""" + reason: Optional[str] + """The justification the impersonator gave for impersonating the user.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "UserSessionsImpersonator": + """Deserialize from a dictionary.""" + try: + return cls( + email=data["email"], + reason=data["reason"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UserSessionsImpersonator: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["email"] = self.email + if self.reason is not None: + result["reason"] = self.reason + else: + result["reason"] = None + return result diff --git a/src/workos/user_management/users/models/user_sessions_list_item.py b/src/workos/user_management/users/models/user_sessions_list_item.py new file mode 100644 index 00000000..0a14554c --- /dev/null +++ b/src/workos/user_management/users/models/user_sessions_list_item.py @@ -0,0 +1,118 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import cast +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + +from .user_sessions_impersonator import UserSessionsImpersonator +from workos.common.models import UserSessionsAuthMethod +from workos.common.models import UserSessionsStatus + + +@dataclass(slots=True) +class UserSessionsListItem: + """User Sessions List Item model.""" + + object: Literal["session"] + """Distinguishes the session object.""" + id: str + """The unique ID of the session.""" + ip_address: Optional[str] + """The IP address from which the session was created.""" + user_agent: Optional[str] + """The user agent string from the device that created the session.""" + user_id: str + """The ID of the user this session belongs to.""" + auth_method: "UserSessionsAuthMethod" + """The authentication method used to create this session.""" + status: "UserSessionsStatus" + """The current status of the session.""" + expires_at: datetime + """The timestamp when the session expires.""" + ended_at: Optional[datetime] + """The timestamp when the session ended.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + impersonator: Optional["UserSessionsImpersonator"] = None + """Information about the impersonator if this session was created via impersonation.""" + organization_id: Optional[str] = None + """The ID of the organization this session is associated with.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "UserSessionsListItem": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + ip_address=data["ip_address"], + user_agent=data["user_agent"], + user_id=data["user_id"], + auth_method=UserSessionsAuthMethod(data["auth_method"]), + status=UserSessionsStatus(data["status"]), + expires_at=datetime.fromisoformat( + data["expires_at"].replace("Z", "+00:00") + ), + ended_at=datetime.fromisoformat(_v.replace("Z", "+00:00")) + if (_v := data["ended_at"]) is not None + else None, + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + impersonator=UserSessionsImpersonator.from_dict( + cast(Dict[str, Any], _v) + ) + if (_v := data.get("impersonator")) is not None + else None, + organization_id=data.get("organization_id"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UserSessionsListItem: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + if self.ip_address is not None: + result["ip_address"] = self.ip_address + else: + result["ip_address"] = None + if self.user_agent is not None: + result["user_agent"] = self.user_agent + else: + result["user_agent"] = None + result["user_id"] = self.user_id + result["auth_method"] = self.auth_method + result["status"] = self.status + result["expires_at"] = self.expires_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + if self.ended_at is not None: + result["ended_at"] = self.ended_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + else: + result["ended_at"] = None + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + if self.impersonator is not None: + result["impersonator"] = self.impersonator.to_dict() + if self.organization_id is not None: + result["organization_id"] = self.organization_id + return result diff --git a/src/workos/user_management/users/models/verify_email_address.py b/src/workos/user_management/users/models/verify_email_address.py new file mode 100644 index 00000000..b5da92b7 --- /dev/null +++ b/src/workos/user_management/users/models/verify_email_address.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class VerifyEmailAddress: + """Verify Email Address model.""" + + code: str + """The one-time email verification code.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "VerifyEmailAddress": + """Deserialize from a dictionary.""" + try: + return cls( + code=data["code"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing VerifyEmailAddress: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["code"] = self.code + return result diff --git a/src/workos/user_management/users/models/verify_email_response.py b/src/workos/user_management/users/models/verify_email_response.py new file mode 100644 index 00000000..2aceb884 --- /dev/null +++ b/src/workos/user_management/users/models/verify_email_response.py @@ -0,0 +1,36 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict +from workos._errors import BaseRequestException + +from workos.user_management.authentication.models import User + + +@dataclass(slots=True) +class VerifyEmailResponse: + """Verify Email Response model.""" + + user: "User" + """The user whose email was verified.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "VerifyEmailResponse": + """Deserialize from a dictionary.""" + try: + return cls( + user=User.from_dict(cast(Dict[str, Any], data["user"])), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing VerifyEmailResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["user"] = self.user.to_dict() + return result diff --git a/src/workos/user_management_users/__init__.py b/src/workos/user_management_users/__init__.py new file mode 100644 index 00000000..33e9c7b7 --- /dev/null +++ b/src/workos/user_management_users/__init__.py @@ -0,0 +1,2 @@ +# This file is auto-generated by oagen. Do not edit. + diff --git a/src/workos/user_management_users/authorized_applications/__init__.py b/src/workos/user_management_users/authorized_applications/__init__.py new file mode 100644 index 00000000..24789054 --- /dev/null +++ b/src/workos/user_management_users/authorized_applications/__init__.py @@ -0,0 +1,7 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import ( + UserManagementUsersAuthorizedApplications, + AsyncUserManagementUsersAuthorizedApplications, +) +from .models import * diff --git a/src/workos/user_management_users/authorized_applications/_resource.py b/src/workos/user_management_users/authorized_applications/_resource.py new file mode 100644 index 00000000..ee207fff --- /dev/null +++ b/src/workos/user_management_users/authorized_applications/_resource.py @@ -0,0 +1,183 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from ..._client import AsyncWorkOSClient, WorkOSClient + +from .models import AuthorizedConnectApplicationListData +from .models import UserManagementUsersAuthorizedApplicationsOrder +from ..._pagination import AsyncPage, SyncPage +from ..._types import RequestOptions + + +class UserManagementUsersAuthorizedApplications: + """User Management Users Authorized Applications API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def list( + self, + user_id: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[UserManagementUsersAuthorizedApplicationsOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[AuthorizedConnectApplicationListData]: + """List authorized applications + + Get a list of all Connect applications that the user has authorized. + + Args: + user_id: The ID of the user. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[AuthorizedConnectApplicationListData] + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path=f"user_management/users/{user_id}/authorized_applications", + model=AuthorizedConnectApplicationListData, + params=params, + request_options=request_options, + ) + + def delete( + self, + application_id: str, + user_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete an authorized application + + Delete an existing Authorized Connect Application. + + Args: + application_id: The ID or client ID of the application. + user_id: The ID of the user. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + self._client.request( + method="delete", + path=f"user_management/users/{user_id}/authorized_applications/{application_id}", + request_options=request_options, + ) + + +class AsyncUserManagementUsersAuthorizedApplications: + """User Management Users Authorized Applications API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def list( + self, + user_id: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[UserManagementUsersAuthorizedApplicationsOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[AuthorizedConnectApplicationListData]: + """List authorized applications + + Get a list of all Connect applications that the user has authorized. + + Args: + user_id: The ID of the user. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[AuthorizedConnectApplicationListData] + + Raises: + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path=f"user_management/users/{user_id}/authorized_applications", + model=AuthorizedConnectApplicationListData, + params=params, + request_options=request_options, + ) + + async def delete( + self, + application_id: str, + user_id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete an authorized application + + Delete an existing Authorized Connect Application. + + Args: + application_id: The ID or client ID of the application. + user_id: The ID of the user. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + await self._client.request( + method="delete", + path=f"user_management/users/{user_id}/authorized_applications/{application_id}", + request_options=request_options, + ) diff --git a/src/workos/user_management_users/authorized_applications/models/__init__.py b/src/workos/user_management_users/authorized_applications/models/__init__.py new file mode 100644 index 00000000..2969c104 --- /dev/null +++ b/src/workos/user_management_users/authorized_applications/models/__init__.py @@ -0,0 +1,8 @@ +# This file is auto-generated by oagen. Do not edit. + +from .authorized_connect_application_list_data import ( + AuthorizedConnectApplicationListData as AuthorizedConnectApplicationListData, +) +from .user_management_users_authorized_applications_order import ( + UserManagementUsersAuthorizedApplicationsOrder as UserManagementUsersAuthorizedApplicationsOrder, +) diff --git a/src/workos/user_management_users/authorized_applications/models/authorized_connect_application_list_data.py b/src/workos/user_management_users/authorized_applications/models/authorized_connect_application_list_data.py new file mode 100644 index 00000000..20a7d493 --- /dev/null +++ b/src/workos/user_management_users/authorized_applications/models/authorized_connect_application_list_data.py @@ -0,0 +1,49 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, List, Literal +from workos._errors import BaseRequestException + +from workos.applications.models import ConnectApplication + + +@dataclass(slots=True) +class AuthorizedConnectApplicationListData: + """Authorized Connect Application List Data model.""" + + object: Literal["authorized_connect_application"] + """Distinguishes the authorized connect application object.""" + id: str + """The unique ID of the authorized connect application.""" + granted_scopes: List[str] + """The scopes granted by the user to the application.""" + application: "ConnectApplication" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "AuthorizedConnectApplicationListData": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + granted_scopes=data["granted_scopes"], + application=ConnectApplication.from_dict( + cast(Dict[str, Any], data["application"]) + ), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing AuthorizedConnectApplicationListData: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["granted_scopes"] = self.granted_scopes + result["application"] = self.application.to_dict() + return result diff --git a/src/workos/user_management_users/authorized_applications/models/user_management_users_authorized_applications_order.py b/src/workos/user_management_users/authorized_applications/models/user_management_users_authorized_applications_order.py new file mode 100644 index 00000000..081f33bb --- /dev/null +++ b/src/workos/user_management_users/authorized_applications/models/user_management_users_authorized_applications_order.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import ApplicationsOrder + +UserManagementUsersAuthorizedApplicationsOrder = ApplicationsOrder +__all__ = ["UserManagementUsersAuthorizedApplicationsOrder"] diff --git a/src/workos/user_management_users/feature_flags/__init__.py b/src/workos/user_management_users/feature_flags/__init__.py new file mode 100644 index 00000000..bb6f3c75 --- /dev/null +++ b/src/workos/user_management_users/feature_flags/__init__.py @@ -0,0 +1,7 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import ( + UserManagementUsersFeatureFlags, + AsyncUserManagementUsersFeatureFlags, +) +from .models import * diff --git a/src/workos/user_management_users/feature_flags/_resource.py b/src/workos/user_management_users/feature_flags/_resource.py new file mode 100644 index 00000000..b0a7f4e5 --- /dev/null +++ b/src/workos/user_management_users/feature_flags/_resource.py @@ -0,0 +1,125 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from ..._client import AsyncWorkOSClient, WorkOSClient + +from workos.feature_flags.models import Flag +from .models import UserManagementUsersFeatureFlagsOrder +from ..._pagination import AsyncPage, SyncPage +from ..._types import RequestOptions + + +class UserManagementUsersFeatureFlags: + """User Management Users Feature Flags API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def list( + self, + user_id: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[UserManagementUsersFeatureFlagsOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[Flag]: + """List enabled feature flags for a user + + Get a list of all enabled feature flags for the provided user. This includes feature flags enabled specifically for the user as well as any organizations that the user is a member of. + + Args: + user_id: The ID of the user. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[Flag] + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path=f"user_management/users/{user_id}/feature-flags", + model=Flag, + params=params, + request_options=request_options, + ) + + +class AsyncUserManagementUsersFeatureFlags: + """User Management Users Feature Flags API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def list( + self, + user_id: str, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[UserManagementUsersFeatureFlagsOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[Flag]: + """List enabled feature flags for a user + + Get a list of all enabled feature flags for the provided user. This includes feature flags enabled specifically for the user as well as any organizations that the user is a member of. + + Args: + user_id: The ID of the user. + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[Flag] + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path=f"user_management/users/{user_id}/feature-flags", + model=Flag, + params=params, + request_options=request_options, + ) diff --git a/src/workos/user_management_users/feature_flags/models/__init__.py b/src/workos/user_management_users/feature_flags/models/__init__.py new file mode 100644 index 00000000..816318a0 --- /dev/null +++ b/src/workos/user_management_users/feature_flags/models/__init__.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from .user_management_users_feature_flags_order import ( + UserManagementUsersFeatureFlagsOrder as UserManagementUsersFeatureFlagsOrder, +) diff --git a/src/workos/user_management_users/feature_flags/models/user_management_users_feature_flags_order.py b/src/workos/user_management_users/feature_flags/models/user_management_users_feature_flags_order.py new file mode 100644 index 00000000..4f10d45e --- /dev/null +++ b/src/workos/user_management_users/feature_flags/models/user_management_users_feature_flags_order.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import ApplicationsOrder + +UserManagementUsersFeatureFlagsOrder = ApplicationsOrder +__all__ = ["UserManagementUsersFeatureFlagsOrder"] diff --git a/src/workos/webhooks/__init__.py b/src/workos/webhooks/__init__.py new file mode 100644 index 00000000..359eb9b9 --- /dev/null +++ b/src/workos/webhooks/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import Webhooks, AsyncWebhooks +from .models import * diff --git a/src/workos/webhooks/_resource.py b/src/workos/webhooks/_resource.py new file mode 100644 index 00000000..17317705 --- /dev/null +++ b/src/workos/webhooks/_resource.py @@ -0,0 +1,441 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +import hashlib +import hmac +import json +import time +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import WebhookEndpointJson +from .models import WebhooksOrder +from workos.common.models import ( + CreateWebhookEndpointDtoEvents, + UpdateWebhookEndpointDtoEvents, + UpdateWebhookEndpointDtoStatus, +) +from .._pagination import AsyncPage, SyncPage +from .._types import RequestOptions + + +class Webhooks: + """Webhooks API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[WebhooksOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> SyncPage[WebhookEndpointJson]: + """List Webhook Endpoints + + Get a list of all of your existing webhook endpoints. + + Args: + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + SyncPage[WebhookEndpointJson] + + Raises: + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return self._client.request_page( + method="get", + path="webhook_endpoints", + model=WebhookEndpointJson, + params=params, + request_options=request_options, + ) + + def create( + self, + *, + endpoint_url: str, + events: List[CreateWebhookEndpointDtoEvents], + request_options: Optional[RequestOptions] = None, + ) -> WebhookEndpointJson: + """Create a Webhook Endpoint + + Create a new webhook endpoint to receive event notifications. + + Args: + endpoint_url: The HTTPS URL where webhooks will be sent. + events: The events that the Webhook Endpoint is subscribed to. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + WebhookEndpointJson + + Raises: + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "endpoint_url": endpoint_url, + "events": events, + } + return self._client.request( + method="post", + path="webhook_endpoints", + body=body, + model=WebhookEndpointJson, + request_options=request_options, + ) + + def update( + self, + id: str, + *, + endpoint_url: Optional[str] = None, + status: Optional[UpdateWebhookEndpointDtoStatus] = None, + events: Optional[List[UpdateWebhookEndpointDtoEvents]] = None, + request_options: Optional[RequestOptions] = None, + ) -> WebhookEndpointJson: + """Update a Webhook Endpoint + + Update the properties of an existing webhook endpoint. + + Args: + id: Unique identifier of the Webhook Endpoint. + endpoint_url: The HTTPS URL where webhooks will be sent. + status: Whether the Webhook Endpoint is enabled or disabled. + events: The events that the Webhook Endpoint is subscribed to. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + WebhookEndpointJson + + Raises: + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "endpoint_url": endpoint_url, + "status": status, + "events": events, + }.items() + if v is not None + } + return self._client.request( + method="patch", + path=f"webhook_endpoints/{id}", + body=body, + model=WebhookEndpointJson, + request_options=request_options, + ) + + def delete( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a Webhook Endpoint + + Delete an existing webhook endpoint. + + Args: + id: Unique identifier of the Webhook Endpoint. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + self._client.request( + method="delete", + path=f"webhook_endpoints/{id}", + request_options=request_options, + ) + + # @oagen-ignore-start + DEFAULT_TOLERANCE = 180 + + def verify_event( + self, + *, + event_body: Union[bytes, str], + event_signature: str, + secret: str, + tolerance: Optional[int] = None, + ) -> Dict[str, Any]: + """Verify and deserialize the signature of a Webhook event. + + Args: + event_body: The raw Webhook request body (bytes or str). + event_signature: The signature from the 'WorkOS-Signature' header. + secret: The webhook endpoint secret from the WorkOS dashboard. + tolerance: Maximum age of the event in seconds. Defaults to 180. + + Returns: + Dict[str, Any]: The deserialized webhook event payload. + + Raises: + ValueError: If the signature is invalid or the event is too old. + """ + self.verify_header( + event_body=event_body, + event_signature=event_signature, + secret=secret, + tolerance=tolerance, + ) + body = event_body if isinstance(event_body, (str, bytes)) else str(event_body) + return json.loads(body) + + def verify_header( + self, + *, + event_body: Union[bytes, str], + event_signature: str, + secret: str, + tolerance: Optional[int] = None, + ) -> None: + """Verify the signature of a Webhook, raise ValueError if invalid. + + Args: + event_body: The raw Webhook request body (bytes or str). + event_signature: The signature from the 'WorkOS-Signature' header. + secret: The webhook endpoint secret from the WorkOS dashboard. + tolerance: Maximum age of the event in seconds. Defaults to 180. + + Raises: + ValueError: If the signature cannot be verified or the event is too old. + """ + try: + issued_timestamp, signature_hash = event_signature.split(", ") + except (ValueError, AttributeError) as exc: + raise ValueError( + "Unable to extract timestamp and signature hash from header", + event_signature, + ) from exc + + issued_timestamp = issued_timestamp[2:] + signature_hash = signature_hash[3:] + max_seconds_since_issued = tolerance if tolerance is not None else self.DEFAULT_TOLERANCE + current_time = time.time() + timestamp_in_seconds = int(issued_timestamp) / 1000 + seconds_since_issued = current_time - timestamp_in_seconds + + if seconds_since_issued > max_seconds_since_issued: + raise ValueError("Timestamp outside the tolerance zone") + + body_str = ( + event_body.decode("utf-8") + if isinstance(event_body, bytes) + else event_body + ) + unhashed_string = f"{issued_timestamp}.{body_str}" + expected_signature = hmac.new( + secret.encode("utf-8"), + unhashed_string.encode("utf-8"), + digestmod=hashlib.sha256, + ).hexdigest() + + if not hmac.compare_digest(signature_hash, expected_signature): + raise ValueError( + "Signature hash does not match the expected signature hash for payload" + ) + # @oagen-ignore-end + + +class AsyncWebhooks: + """Webhooks API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def list( + self, + *, + limit: Optional[int] = None, + before: Optional[str] = None, + after: Optional[str] = None, + order: Optional[WebhooksOrder] = None, + request_options: Optional[RequestOptions] = None, + ) -> AsyncPage[WebhookEndpointJson]: + """List Webhook Endpoints + + Get a list of all of your existing webhook endpoints. + + Args: + limit: Maximum number of records to return. + before: Pagination cursor for previous page. + after: Pagination cursor for next page. + order: Sort order. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + AsyncPage[WebhookEndpointJson] + + Raises: + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + params = { + k: v + for k, v in { + "limit": limit, + "before": before, + "after": after, + "order": order.value if order else None, + }.items() + if v is not None + } + return await self._client.request_page( + method="get", + path="webhook_endpoints", + model=WebhookEndpointJson, + params=params, + request_options=request_options, + ) + + async def create( + self, + *, + endpoint_url: str, + events: List[CreateWebhookEndpointDtoEvents], + request_options: Optional[RequestOptions] = None, + ) -> WebhookEndpointJson: + """Create a Webhook Endpoint + + Create a new webhook endpoint to receive event notifications. + + Args: + endpoint_url: The HTTPS URL where webhooks will be sent. + events: The events that the Webhook Endpoint is subscribed to. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + WebhookEndpointJson + + Raises: + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + "endpoint_url": endpoint_url, + "events": events, + } + return await self._client.request( + method="post", + path="webhook_endpoints", + body=body, + model=WebhookEndpointJson, + request_options=request_options, + ) + + async def update( + self, + id: str, + *, + endpoint_url: Optional[str] = None, + status: Optional[UpdateWebhookEndpointDtoStatus] = None, + events: Optional[List[UpdateWebhookEndpointDtoEvents]] = None, + request_options: Optional[RequestOptions] = None, + ) -> WebhookEndpointJson: + """Update a Webhook Endpoint + + Update the properties of an existing webhook endpoint. + + Args: + id: Unique identifier of the Webhook Endpoint. + endpoint_url: The HTTPS URL where webhooks will be sent. + status: Whether the Webhook Endpoint is enabled or disabled. + events: The events that the Webhook Endpoint is subscribed to. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + WebhookEndpointJson + + Raises: + NotFoundException: If the resource is not found (404). + ConflictException: If a conflict occurs (409). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "endpoint_url": endpoint_url, + "status": status, + "events": events, + }.items() + if v is not None + } + return await self._client.request( + method="patch", + path=f"webhook_endpoints/{id}", + body=body, + model=WebhookEndpointJson, + request_options=request_options, + ) + + async def delete( + self, + id: str, + *, + request_options: Optional[RequestOptions] = None, + ) -> None: + """Delete a Webhook Endpoint + + Delete an existing webhook endpoint. + + Args: + id: Unique identifier of the Webhook Endpoint. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Raises: + NotFoundException: If the resource is not found (404). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + await self._client.request( + method="delete", + path=f"webhook_endpoints/{id}", + request_options=request_options, + ) diff --git a/src/workos/webhooks/models/__init__.py b/src/workos/webhooks/models/__init__.py new file mode 100644 index 00000000..298f1958 --- /dev/null +++ b/src/workos/webhooks/models/__init__.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .create_webhook_endpoint import CreateWebhookEndpoint as CreateWebhookEndpoint +from .update_webhook_endpoint import UpdateWebhookEndpoint as UpdateWebhookEndpoint +from .webhook_endpoint_json import WebhookEndpointJson as WebhookEndpointJson +from .webhooks_order import WebhooksOrder as WebhooksOrder diff --git a/src/workos/webhooks/models/create_webhook_endpoint.py b/src/workos/webhooks/models/create_webhook_endpoint.py new file mode 100644 index 00000000..233142e9 --- /dev/null +++ b/src/workos/webhooks/models/create_webhook_endpoint.py @@ -0,0 +1,42 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, List +from workos._errors import BaseRequestException +from workos.common.models import CreateWebhookEndpointDtoEvents + + +@dataclass(slots=True) +class CreateWebhookEndpoint: + """Create Webhook Endpoint model.""" + + endpoint_url: str + """The HTTPS URL where webhooks will be sent.""" + events: List["CreateWebhookEndpointDtoEvents"] + """The events that the Webhook Endpoint is subscribed to.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "CreateWebhookEndpoint": + """Deserialize from a dictionary.""" + try: + return cls( + endpoint_url=data["endpoint_url"], + events=[ + CreateWebhookEndpointDtoEvents(item) + for item in cast(list[Any], data["events"]) + ], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing CreateWebhookEndpoint: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["endpoint_url"] = self.endpoint_url + result["events"] = self.events + return result diff --git a/src/workos/webhooks/models/update_webhook_endpoint.py b/src/workos/webhooks/models/update_webhook_endpoint.py new file mode 100644 index 00000000..a414a4fe --- /dev/null +++ b/src/workos/webhooks/models/update_webhook_endpoint.py @@ -0,0 +1,53 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, List, Optional +from workos._errors import BaseRequestException +from workos.common.models import UpdateWebhookEndpointDtoEvents +from workos.common.models import UpdateWebhookEndpointDtoStatus + + +@dataclass(slots=True) +class UpdateWebhookEndpoint: + """Update Webhook Endpoint model.""" + + endpoint_url: Optional[str] = None + """The HTTPS URL where webhooks will be sent.""" + status: Optional["UpdateWebhookEndpointDtoStatus"] = None + """Whether the Webhook Endpoint is enabled or disabled.""" + events: Optional[List["UpdateWebhookEndpointDtoEvents"]] = None + """The events that the Webhook Endpoint is subscribed to.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "UpdateWebhookEndpoint": + """Deserialize from a dictionary.""" + try: + return cls( + endpoint_url=data.get("endpoint_url"), + status=UpdateWebhookEndpointDtoStatus(_v) + if (_v := data.get("status")) is not None + else None, + events=[ + UpdateWebhookEndpointDtoEvents(item) for item in cast(list[Any], _v) + ] + if (_v := data.get("events")) is not None + else None, + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UpdateWebhookEndpoint: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + if self.endpoint_url is not None: + result["endpoint_url"] = self.endpoint_url + if self.status is not None: + result["status"] = self.status + if self.events is not None: + result["events"] = self.events + return result diff --git a/src/workos/webhooks/models/webhook_endpoint_json.py b/src/workos/webhooks/models/webhook_endpoint_json.py new file mode 100644 index 00000000..d3d528b8 --- /dev/null +++ b/src/workos/webhooks/models/webhook_endpoint_json.py @@ -0,0 +1,71 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, List, Literal +from workos._errors import BaseRequestException +from workos.common.models import WebhookEndpointJsonStatus + + +@dataclass(slots=True) +class WebhookEndpointJson: + """Webhook Endpoint Json model.""" + + object: Literal["webhook_endpoint"] + """Distinguishes the Webhook Endpoint object.""" + id: str + """Unique identifier of the Webhook Endpoint.""" + endpoint_url: str + """The URL to which webhooks are sent.""" + secret: str + """The secret used to sign webhook payloads.""" + status: "WebhookEndpointJsonStatus" + """Whether the Webhook Endpoint is enabled or disabled.""" + events: List[str] + """The events that the Webhook Endpoint is subscribed to.""" + created_at: datetime + """An ISO 8601 timestamp.""" + updated_at: datetime + """An ISO 8601 timestamp.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "WebhookEndpointJson": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + endpoint_url=data["endpoint_url"], + secret=data["secret"], + status=WebhookEndpointJsonStatus(data["status"]), + events=data["events"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + updated_at=datetime.fromisoformat( + data["updated_at"].replace("Z", "+00:00") + ), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing WebhookEndpointJson: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["endpoint_url"] = self.endpoint_url + result["secret"] = self.secret + result["status"] = self.status + result["events"] = self.events + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + result["updated_at"] = self.updated_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + return result diff --git a/src/workos/webhooks/models/webhooks_order.py b/src/workos/webhooks/models/webhooks_order.py new file mode 100644 index 00000000..e020a5d5 --- /dev/null +++ b/src/workos/webhooks/models/webhooks_order.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import ApplicationsOrder + +WebhooksOrder = ApplicationsOrder +__all__ = ["WebhooksOrder"] diff --git a/src/workos/widgets/__init__.py b/src/workos/widgets/__init__.py new file mode 100644 index 00000000..2f2e3801 --- /dev/null +++ b/src/workos/widgets/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import Widgets, AsyncWidgets +from .models import * diff --git a/src/workos/widgets/_resource.py b/src/workos/widgets/_resource.py new file mode 100644 index 00000000..530ee6b2 --- /dev/null +++ b/src/workos/widgets/_resource.py @@ -0,0 +1,118 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import WidgetSessionTokenResponse +from workos.common.models import WidgetSessionTokenDtoScopes +from .._types import RequestOptions + + +class Widgets: + """Widgets API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def issue_widget_session_token( + self, + *, + organization_id: str, + user_id: Optional[str] = None, + scopes: Optional[List[WidgetSessionTokenDtoScopes]] = None, + request_options: Optional[RequestOptions] = None, + ) -> WidgetSessionTokenResponse: + """Generate a widget token + + Generate a widget token scoped to an organization and user with the specified scopes. + + Args: + organization_id: The ID of the organization to scope the widget session to. + user_id: The ID of the user to issue the widget session token for. + scopes: The scopes to grant the widget session. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + WidgetSessionTokenResponse + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "organization_id": organization_id, + "user_id": user_id, + "scopes": scopes, + }.items() + if v is not None + } + return self._client.request( + method="post", + path="widgets/token", + body=body, + model=WidgetSessionTokenResponse, + request_options=request_options, + ) + + +class AsyncWidgets: + """Widgets API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def issue_widget_session_token( + self, + *, + organization_id: str, + user_id: Optional[str] = None, + scopes: Optional[List[WidgetSessionTokenDtoScopes]] = None, + request_options: Optional[RequestOptions] = None, + ) -> WidgetSessionTokenResponse: + """Generate a widget token + + Generate a widget token scoped to an organization and user with the specified scopes. + + Args: + organization_id: The ID of the organization to scope the widget session to. + user_id: The ID of the user to issue the widget session token for. + scopes: The scopes to grant the widget session. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + WidgetSessionTokenResponse + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "organization_id": organization_id, + "user_id": user_id, + "scopes": scopes, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path="widgets/token", + body=body, + model=WidgetSessionTokenResponse, + request_options=request_options, + ) diff --git a/src/workos/widgets/models/__init__.py b/src/workos/widgets/models/__init__.py new file mode 100644 index 00000000..a55c90f7 --- /dev/null +++ b/src/workos/widgets/models/__init__.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .widget_session_token import WidgetSessionToken as WidgetSessionToken +from .widget_session_token_response import ( + WidgetSessionTokenResponse as WidgetSessionTokenResponse, +) diff --git a/src/workos/widgets/models/widget_session_token.py b/src/workos/widgets/models/widget_session_token.py new file mode 100644 index 00000000..9560e52b --- /dev/null +++ b/src/workos/widgets/models/widget_session_token.py @@ -0,0 +1,49 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, List, Optional +from workos._errors import BaseRequestException +from workos.common.models import WidgetSessionTokenDtoScopes + + +@dataclass(slots=True) +class WidgetSessionToken: + """Widget Session Token model.""" + + organization_id: str + """The ID of the organization to scope the widget session to.""" + user_id: Optional[str] = None + """The ID of the user to issue the widget session token for.""" + scopes: Optional[List["WidgetSessionTokenDtoScopes"]] = None + """The scopes to grant the widget session.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "WidgetSessionToken": + """Deserialize from a dictionary.""" + try: + return cls( + organization_id=data["organization_id"], + user_id=data.get("user_id"), + scopes=[ + WidgetSessionTokenDtoScopes(item) for item in cast(list[Any], _v) + ] + if (_v := data.get("scopes")) is not None + else None, + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing WidgetSessionToken: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["organization_id"] = self.organization_id + if self.user_id is not None: + result["user_id"] = self.user_id + if self.scopes is not None: + result["scopes"] = self.scopes + return result diff --git a/src/workos/widgets/models/widget_session_token_response.py b/src/workos/widgets/models/widget_session_token_response.py new file mode 100644 index 00000000..b7817c58 --- /dev/null +++ b/src/workos/widgets/models/widget_session_token_response.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class WidgetSessionTokenResponse: + """Widget Session Token Response model.""" + + token: str + """The widget session token.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "WidgetSessionTokenResponse": + """Deserialize from a dictionary.""" + try: + return cls( + token=data["token"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing WidgetSessionTokenResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["token"] = self.token + return result diff --git a/src/workos/workos_connect/__init__.py b/src/workos/workos_connect/__init__.py new file mode 100644 index 00000000..034b5385 --- /dev/null +++ b/src/workos/workos_connect/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from ._resource import WorkosConnect, AsyncWorkosConnect +from .models import * diff --git a/src/workos/workos_connect/_resource.py b/src/workos/workos_connect/_resource.py new file mode 100644 index 00000000..9e365aa0 --- /dev/null +++ b/src/workos/workos_connect/_resource.py @@ -0,0 +1,143 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Dict, List, Optional + +if TYPE_CHECKING: + from .._client import AsyncWorkOSClient, WorkOSClient + +from .models import ExternalAuthCompleteResponse, UserConsentOption, UserObject +from .._types import RequestOptions + + +class WorkosConnect: + """Workos Connect API resources.""" + + def __init__(self, client: "WorkOSClient") -> None: + self._client = client + + def complete_login( + self, + *, + external_auth_id: str, + user: UserObject, + user_consent_options: Optional[List[UserConsentOption]] = None, + request_options: Optional[RequestOptions] = None, + ) -> ExternalAuthCompleteResponse: + """Complete external authentication + + Completes an external authentication flow and returns control to AuthKit. This endpoint is used with [Standalone Connect](https://workos.com/docs/authkit/connect/standalone) to bridge your existing authentication system with the Connect OAuth API infrastructure. + + After successfully authenticating a user in your application, calling this endpoint will: + + - Create or update the user in AuthKit, using the given `id` as its `external_id`. + - Return a `redirect_uri` your application should redirect to in order for AuthKit to complete the flow + + Users are automatically created or updated based on the `id` and `email` provided. If a user with the same `id` exists, their information is updated. Otherwise, a new user is created. + + If you provide a new `id` with an `email` that already belongs to an existing user, the request will fail with an error as email addresses are unique to a user. + + Args: + external_auth_id: Identifier provided when AuthKit redirected to your login page. + user: The user to create or update in AuthKit. + user_consent_options: Array of [User Consent Options](https://workos.com/docs/reference/workos-connect/standalone/user-consent-options) to store with the session. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + ExternalAuthCompleteResponse + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "external_auth_id": external_auth_id, + "user": user.to_dict(), + "user_consent_options": [ + item.to_dict() for item in user_consent_options + ] + if user_consent_options is not None + else None, + }.items() + if v is not None + } + return self._client.request( + method="post", + path="authkit/oauth2/complete", + body=body, + model=ExternalAuthCompleteResponse, + request_options=request_options, + ) + + +class AsyncWorkosConnect: + """Workos Connect API resources (async).""" + + def __init__(self, client: "AsyncWorkOSClient") -> None: + self._client = client + + async def complete_login( + self, + *, + external_auth_id: str, + user: UserObject, + user_consent_options: Optional[List[UserConsentOption]] = None, + request_options: Optional[RequestOptions] = None, + ) -> ExternalAuthCompleteResponse: + """Complete external authentication + + Completes an external authentication flow and returns control to AuthKit. This endpoint is used with [Standalone Connect](https://workos.com/docs/authkit/connect/standalone) to bridge your existing authentication system with the Connect OAuth API infrastructure. + + After successfully authenticating a user in your application, calling this endpoint will: + + - Create or update the user in AuthKit, using the given `id` as its `external_id`. + - Return a `redirect_uri` your application should redirect to in order for AuthKit to complete the flow + + Users are automatically created or updated based on the `id` and `email` provided. If a user with the same `id` exists, their information is updated. Otherwise, a new user is created. + + If you provide a new `id` with an `email` that already belongs to an existing user, the request will fail with an error as email addresses are unique to a user. + + Args: + external_auth_id: Identifier provided when AuthKit redirected to your login page. + user: The user to create or update in AuthKit. + user_consent_options: Array of [User Consent Options](https://workos.com/docs/reference/workos-connect/standalone/user-consent-options) to store with the session. + request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. + + Returns: + ExternalAuthCompleteResponse + + Raises: + BadRequestException: If the request is malformed (400). + NotFoundException: If the resource is not found (404). + UnprocessableEntityException: If the request data is unprocessable (422). + AuthenticationException: If the API key is invalid (401). + RateLimitExceededException: If rate limited (429). + ServerException: If the server returns a 5xx error. + """ + body: Dict[str, Any] = { + k: v + for k, v in { + "external_auth_id": external_auth_id, + "user": user.to_dict(), + "user_consent_options": [ + item.to_dict() for item in user_consent_options + ] + if user_consent_options is not None + else None, + }.items() + if v is not None + } + return await self._client.request( + method="post", + path="authkit/oauth2/complete", + body=body, + model=ExternalAuthCompleteResponse, + request_options=request_options, + ) diff --git a/src/workos/workos_connect/models/__init__.py b/src/workos/workos_connect/models/__init__.py new file mode 100644 index 00000000..a0675dc3 --- /dev/null +++ b/src/workos/workos_connect/models/__init__.py @@ -0,0 +1,13 @@ +# This file is auto-generated by oagen. Do not edit. + +from .external_auth_complete_response import ( + ExternalAuthCompleteResponse as ExternalAuthCompleteResponse, +) +from .user_consent_option import UserConsentOption as UserConsentOption +from .user_consent_option_choice import ( + UserConsentOptionChoice as UserConsentOptionChoice, +) +from .user_management_login_request import ( + UserManagementLoginRequest as UserManagementLoginRequest, +) +from .user_object import UserObject as UserObject diff --git a/src/workos/workos_connect/models/external_auth_complete_response.py b/src/workos/workos_connect/models/external_auth_complete_response.py new file mode 100644 index 00000000..c997af07 --- /dev/null +++ b/src/workos/workos_connect/models/external_auth_complete_response.py @@ -0,0 +1,33 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class ExternalAuthCompleteResponse: + """External Auth Complete Response model.""" + + redirect_uri: str + """URI to redirect the user back to AuthKit to complete the OAuth flow.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "ExternalAuthCompleteResponse": + """Deserialize from a dictionary.""" + try: + return cls( + redirect_uri=data["redirect_uri"], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing ExternalAuthCompleteResponse: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["redirect_uri"] = self.redirect_uri + return result diff --git a/src/workos/workos_connect/models/user_consent_option.py b/src/workos/workos_connect/models/user_consent_option.py new file mode 100644 index 00000000..46338914 --- /dev/null +++ b/src/workos/workos_connect/models/user_consent_option.py @@ -0,0 +1,51 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, List, Literal +from workos._errors import BaseRequestException + +from .user_consent_option_choice import UserConsentOptionChoice + + +@dataclass(slots=True) +class UserConsentOption: + """User Consent Option model.""" + + claim: str + """The claim name for this consent option.""" + type: Literal["enum"] + """The type of consent option.""" + label: str + """A human-readable label for this consent option.""" + choices: List["UserConsentOptionChoice"] + """The available choices for this consent option.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "UserConsentOption": + """Deserialize from a dictionary.""" + try: + return cls( + claim=data["claim"], + type=data["type"], + label=data["label"], + choices=[ + UserConsentOptionChoice.from_dict(cast(Dict[str, Any], item)) + for item in cast(list[Any], data["choices"]) + ], + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UserConsentOption: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["claim"] = self.claim + result["type"] = self.type + result["label"] = self.label + result["choices"] = [item.to_dict() for item in self.choices] + return result diff --git a/src/workos/workos_connect/models/user_consent_option_choice.py b/src/workos/workos_connect/models/user_consent_option_choice.py new file mode 100644 index 00000000..6c4ac8a2 --- /dev/null +++ b/src/workos/workos_connect/models/user_consent_option_choice.py @@ -0,0 +1,39 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class UserConsentOptionChoice: + """User Consent Option Choice model.""" + + value: Optional[str] = None + """The value of this choice.""" + label: Optional[str] = None + """A human-readable label for this choice.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "UserConsentOptionChoice": + """Deserialize from a dictionary.""" + try: + return cls( + value=data.get("value"), + label=data.get("label"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UserConsentOptionChoice: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + if self.value is not None: + result["value"] = self.value + if self.label is not None: + result["label"] = self.label + return result diff --git a/src/workos/workos_connect/models/user_management_login_request.py b/src/workos/workos_connect/models/user_management_login_request.py new file mode 100644 index 00000000..c43b4309 --- /dev/null +++ b/src/workos/workos_connect/models/user_management_login_request.py @@ -0,0 +1,53 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast +from typing import Any, Dict, List, Optional +from workos._errors import BaseRequestException + +from .user_consent_option import UserConsentOption +from .user_object import UserObject + + +@dataclass(slots=True) +class UserManagementLoginRequest: + """User Management Login Request model.""" + + external_auth_id: str + """Identifier provided when AuthKit redirected to your login page.""" + user: "UserObject" + """The user to create or update in AuthKit.""" + user_consent_options: Optional[List["UserConsentOption"]] = None + """Array of [User Consent Options](https://workos.com/docs/reference/workos-connect/standalone/user-consent-options) to store with the session.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "UserManagementLoginRequest": + """Deserialize from a dictionary.""" + try: + return cls( + external_auth_id=data["external_auth_id"], + user=UserObject.from_dict(cast(Dict[str, Any], data["user"])), + user_consent_options=[ + UserConsentOption.from_dict(cast(Dict[str, Any], item)) + for item in cast(list[Any], _v) + ] + if (_v := data.get("user_consent_options")) is not None + else None, + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UserManagementLoginRequest: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["external_auth_id"] = self.external_auth_id + result["user"] = self.user.to_dict() + if self.user_consent_options is not None: + result["user_consent_options"] = [ + item.to_dict() for item in self.user_consent_options + ] + return result diff --git a/src/workos/workos_connect/models/user_object.py b/src/workos/workos_connect/models/user_object.py new file mode 100644 index 00000000..b3901f10 --- /dev/null +++ b/src/workos/workos_connect/models/user_object.py @@ -0,0 +1,52 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class UserObject: + """User Object model.""" + + id: str + """Your application's user identifier, which will be stored as an [`external_id`](https://workos.com/docs/authkit/metadata/external-identifiers). Used for upserting and deduplication.""" + email: str + """The user's email address.""" + first_name: Optional[str] = None + """The user's first name.""" + last_name: Optional[str] = None + """The user's last name.""" + metadata: Optional[Dict[str, str]] = None + """A set of key-value pairs to attach to the user.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "UserObject": + """Deserialize from a dictionary.""" + try: + return cls( + id=data["id"], + email=data["email"], + first_name=data.get("first_name"), + last_name=data.get("last_name"), + metadata=data.get("metadata"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing UserObject: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["id"] = self.id + result["email"] = self.email + if self.first_name is not None: + result["first_name"] = self.first_name + if self.last_name is not None: + result["last_name"] = self.last_name + if self.metadata is not None: + result["metadata"] = self.metadata + return result diff --git a/tests/conftest.py b/tests/conftest.py index a30f909b..31d4b4fc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,18 +1,28 @@ +# This file is auto-generated by oagen. Do not edit. + import pytest +import pytest_asyncio + from workos import WorkOS, AsyncWorkOS @pytest.fixture def workos(): - return WorkOS( - api_key="sk_test", - client_id="client_test", + """Create a WorkOS client for testing with guaranteed cleanup.""" + client = WorkOS( + api_key="sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU", client_id="client_test" ) + yield client + client.close() -@pytest.fixture -def async_workos(): - return AsyncWorkOS( - api_key="sk_test", - client_id="client_test", +@pytest_asyncio.fixture +async def async_workos(): + """Create an AsyncWorkOS client for testing with guaranteed cleanup.""" + client = AsyncWorkOS( + api_key="sk_test_Sz3IQjepeSWaI4cMS4ms4sMuU", client_id="client_test" ) + try: + yield client + finally: + await client.close() diff --git a/tests/fixtures/add_role_permission.json b/tests/fixtures/add_role_permission.json new file mode 100644 index 00000000..8dd80ea6 --- /dev/null +++ b/tests/fixtures/add_role_permission.json @@ -0,0 +1,3 @@ +{ + "slug": "reports:export" +} diff --git a/tests/fixtures/api_key.json b/tests/fixtures/api_key.json new file mode 100644 index 00000000..e05b1db2 --- /dev/null +++ b/tests/fixtures/api_key.json @@ -0,0 +1,17 @@ +{ + "object": "api_key", + "id": "api_key_01EHZNVPK3SFK441A1RGBFSHRT", + "owner": { + "type": "organization", + "id": "org_01EHZNVPK3SFK441A1RGBFSHRT" + }, + "name": "Production API Key", + "obfuscated_value": "sk_...3456", + "last_used_at": null, + "permissions": [ + "posts:read", + "posts:write" + ], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/api_key_owner.json b/tests/fixtures/api_key_owner.json new file mode 100644 index 00000000..ddbfe81d --- /dev/null +++ b/tests/fixtures/api_key_owner.json @@ -0,0 +1,4 @@ +{ + "type": "organization", + "id": "org_01EHZNVPK3SFK441A1RGBFSHRT" +} diff --git a/tests/fixtures/api_key_validation_response.json b/tests/fixtures/api_key_validation_response.json new file mode 100644 index 00000000..1e77554f --- /dev/null +++ b/tests/fixtures/api_key_validation_response.json @@ -0,0 +1,19 @@ +{ + "api_key": { + "object": "api_key", + "id": "api_key_01EHZNVPK3SFK441A1RGBFSHRT", + "owner": { + "type": "organization", + "id": "org_01EHZNVPK3SFK441A1RGBFSHRT" + }, + "name": "Production API Key", + "obfuscated_value": "sk_...3456", + "last_used_at": null, + "permissions": [ + "posts:read", + "posts:write" + ], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } +} diff --git a/tests/fixtures/api_key_with_value.json b/tests/fixtures/api_key_with_value.json new file mode 100644 index 00000000..9c85f194 --- /dev/null +++ b/tests/fixtures/api_key_with_value.json @@ -0,0 +1,18 @@ +{ + "object": "api_key", + "id": "api_key_01EHZNVPK3SFK441A1RGBFSHRT", + "owner": { + "type": "organization", + "id": "org_01EHZNVPK3SFK441A1RGBFSHRT" + }, + "name": "Production API Key", + "obfuscated_value": "sk_...3456", + "last_used_at": null, + "permissions": [ + "posts:read", + "posts:write" + ], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "value": "sk_abcdefghijklmnop123456" +} diff --git a/tests/fixtures/api_key_with_value_owner.json b/tests/fixtures/api_key_with_value_owner.json new file mode 100644 index 00000000..ddbfe81d --- /dev/null +++ b/tests/fixtures/api_key_with_value_owner.json @@ -0,0 +1,4 @@ +{ + "type": "organization", + "id": "org_01EHZNVPK3SFK441A1RGBFSHRT" +} diff --git a/tests/fixtures/application_credentials_list_item.json b/tests/fixtures/application_credentials_list_item.json new file mode 100644 index 00000000..4c03cc2e --- /dev/null +++ b/tests/fixtures/application_credentials_list_item.json @@ -0,0 +1,8 @@ +{ + "object": "connect_application_secret", + "id": "secret_01J9Q2Z3X4Y5W6V7U8T9S0R1Q", + "secret_hint": "abc123", + "last_used_at": null, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/assign_role.json b/tests/fixtures/assign_role.json new file mode 100644 index 00000000..66cab23c --- /dev/null +++ b/tests/fixtures/assign_role.json @@ -0,0 +1,6 @@ +{ + "role_slug": "editor", + "resource_id": "authz_resource_01HXYZ123456789ABCDEFGH", + "resource_external_id": "project-ext-456", + "resource_type_slug": "project" +} diff --git a/tests/fixtures/audit_log_action_json.json b/tests/fixtures/audit_log_action_json.json new file mode 100644 index 00000000..a7212b6c --- /dev/null +++ b/tests/fixtures/audit_log_action_json.json @@ -0,0 +1,42 @@ +{ + "object": "audit_log_action", + "name": "user.viewed_invoice", + "schema": { + "object": "audit_log_schema", + "version": 1, + "actor": { + "metadata": { + "type": "object", + "properties": { + "role": { + "type": "string" + } + } + } + }, + "targets": [ + { + "type": "invoice", + "metadata": { + "type": "object", + "properties": { + "cost": { + "type": "number" + } + } + } + } + ], + "metadata": { + "type": "object", + "properties": { + "transactionId": { + "type": "string" + } + } + }, + "created_at": "2026-01-15T12:00:00.000Z" + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/audit_log_configuration.json b/tests/fixtures/audit_log_configuration.json new file mode 100644 index 00000000..fd33f456 --- /dev/null +++ b/tests/fixtures/audit_log_configuration.json @@ -0,0 +1,12 @@ +{ + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "retention_period_in_days": 30, + "state": "active", + "log_stream": { + "id": "als_01EHZNVPK3SFK441A1RGBFSHRT", + "type": "Datadog", + "state": "active", + "last_synced_at": "2026-01-15T12:00:00.000Z", + "created_at": "2026-01-15T12:00:00.000Z" + } +} diff --git a/tests/fixtures/audit_log_configuration_log_stream.json b/tests/fixtures/audit_log_configuration_log_stream.json new file mode 100644 index 00000000..c108bda9 --- /dev/null +++ b/tests/fixtures/audit_log_configuration_log_stream.json @@ -0,0 +1,7 @@ +{ + "id": "als_01EHZNVPK3SFK441A1RGBFSHRT", + "type": "Datadog", + "state": "active", + "last_synced_at": "2026-01-15T12:00:00.000Z", + "created_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/audit_log_event.json b/tests/fixtures/audit_log_event.json new file mode 100644 index 00000000..ab6e39e7 --- /dev/null +++ b/tests/fixtures/audit_log_event.json @@ -0,0 +1,30 @@ +{ + "action": "user.signed_in", + "occurred_at": "2026-02-02T16:35:39.317Z", + "actor": { + "id": "user_TF4C5938", + "type": "user", + "name": "Jon Smith", + "metadata": { + "owner": "user_01GBTCQ2" + } + }, + "targets": [ + { + "id": "user_TF4C5938", + "type": "user", + "name": "Jon Smith", + "metadata": { + "owner": "user_01GBTCQ2" + } + } + ], + "context": { + "location": "123.123.123.123", + "user_agent": "Chrome/104.0.0.0" + }, + "metadata": { + "owner": "user_01GBTCQ2" + }, + "version": 1 +} diff --git a/tests/fixtures/audit_log_event_actor.json b/tests/fixtures/audit_log_event_actor.json new file mode 100644 index 00000000..68894d0d --- /dev/null +++ b/tests/fixtures/audit_log_event_actor.json @@ -0,0 +1,8 @@ +{ + "id": "user_TF4C5938", + "type": "user", + "name": "Jon Smith", + "metadata": { + "owner": "user_01GBTCQ2" + } +} diff --git a/tests/fixtures/audit_log_event_context.json b/tests/fixtures/audit_log_event_context.json new file mode 100644 index 00000000..4b61b917 --- /dev/null +++ b/tests/fixtures/audit_log_event_context.json @@ -0,0 +1,4 @@ +{ + "location": "123.123.123.123", + "user_agent": "Chrome/104.0.0.0" +} diff --git a/tests/fixtures/audit_log_event_create_response.json b/tests/fixtures/audit_log_event_create_response.json new file mode 100644 index 00000000..5550c6db --- /dev/null +++ b/tests/fixtures/audit_log_event_create_response.json @@ -0,0 +1,3 @@ +{ + "success": true +} diff --git a/tests/fixtures/audit_log_event_ingestion.json b/tests/fixtures/audit_log_event_ingestion.json new file mode 100644 index 00000000..09c9f490 --- /dev/null +++ b/tests/fixtures/audit_log_event_ingestion.json @@ -0,0 +1,33 @@ +{ + "organization_id": "org_01EHWNCE74X7JSDV0X3SZ3KJNY", + "event": { + "action": "user.signed_in", + "occurred_at": "2026-02-02T16:35:39.317Z", + "actor": { + "id": "user_TF4C5938", + "type": "user", + "name": "Jon Smith", + "metadata": { + "owner": "user_01GBTCQ2" + } + }, + "targets": [ + { + "id": "user_TF4C5938", + "type": "user", + "name": "Jon Smith", + "metadata": { + "owner": "user_01GBTCQ2" + } + } + ], + "context": { + "location": "123.123.123.123", + "user_agent": "Chrome/104.0.0.0" + }, + "metadata": { + "owner": "user_01GBTCQ2" + }, + "version": 1 + } +} diff --git a/tests/fixtures/audit_log_event_target.json b/tests/fixtures/audit_log_event_target.json new file mode 100644 index 00000000..68894d0d --- /dev/null +++ b/tests/fixtures/audit_log_event_target.json @@ -0,0 +1,8 @@ +{ + "id": "user_TF4C5938", + "type": "user", + "name": "Jon Smith", + "metadata": { + "owner": "user_01GBTCQ2" + } +} diff --git a/tests/fixtures/audit_log_export_creation.json b/tests/fixtures/audit_log_export_creation.json new file mode 100644 index 00000000..8897537d --- /dev/null +++ b/tests/fixtures/audit_log_export_creation.json @@ -0,0 +1,20 @@ +{ + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "range_start": "2022-07-02T18:09:06.996Z", + "range_end": "2022-09-02T18:09:06.996Z", + "actions": [ + "user.signed_in" + ], + "actors": [ + "Jon Smith" + ], + "actor_names": [ + "Jon Smith" + ], + "actor_ids": [ + "user_01GBZK5MP7TD1YCFQHFR22180V" + ], + "targets": [ + "team" + ] +} diff --git a/tests/fixtures/audit_log_export_json.json b/tests/fixtures/audit_log_export_json.json new file mode 100644 index 00000000..e076643f --- /dev/null +++ b/tests/fixtures/audit_log_export_json.json @@ -0,0 +1,8 @@ +{ + "object": "audit_log_export", + "id": "audit_log_export_01GBZK5MP7TD1YCFQHFR22180V", + "state": "ready", + "url": "https://exports.audit-logs.com/audit-log-exports/export.csv", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/audit_log_schema.json b/tests/fixtures/audit_log_schema.json new file mode 100644 index 00000000..773d6b72 --- /dev/null +++ b/tests/fixtures/audit_log_schema.json @@ -0,0 +1,33 @@ +{ + "actor": { + "metadata": { + "type": "object", + "properties": { + "role": { + "type": "string" + } + } + } + }, + "targets": [ + { + "type": "invoice", + "metadata": { + "type": "object", + "properties": { + "cost": { + "type": "number" + } + } + } + } + ], + "metadata": { + "type": "object", + "properties": { + "transactionId": { + "type": "string" + } + } + } +} diff --git a/tests/fixtures/audit_log_schema_actor.json b/tests/fixtures/audit_log_schema_actor.json new file mode 100644 index 00000000..cea87599 --- /dev/null +++ b/tests/fixtures/audit_log_schema_actor.json @@ -0,0 +1,10 @@ +{ + "metadata": { + "type": "object", + "properties": { + "role": { + "type": "string" + } + } + } +} diff --git a/tests/fixtures/audit_log_schema_json.json b/tests/fixtures/audit_log_schema_json.json new file mode 100644 index 00000000..cdf6f075 --- /dev/null +++ b/tests/fixtures/audit_log_schema_json.json @@ -0,0 +1,36 @@ +{ + "object": "audit_log_schema", + "version": 1, + "actor": { + "metadata": { + "type": "object", + "properties": { + "role": { + "type": "string" + } + } + } + }, + "targets": [ + { + "type": "invoice", + "metadata": { + "type": "object", + "properties": { + "cost": { + "type": "number" + } + } + } + } + ], + "metadata": { + "type": "object", + "properties": { + "transactionId": { + "type": "string" + } + } + }, + "created_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/audit_log_schema_json_actor.json b/tests/fixtures/audit_log_schema_json_actor.json new file mode 100644 index 00000000..319068e7 --- /dev/null +++ b/tests/fixtures/audit_log_schema_json_actor.json @@ -0,0 +1,5 @@ +{ + "metadata": { + "key": {} + } +} diff --git a/tests/fixtures/audit_log_schema_json_target.json b/tests/fixtures/audit_log_schema_json_target.json new file mode 100644 index 00000000..328b5256 --- /dev/null +++ b/tests/fixtures/audit_log_schema_json_target.json @@ -0,0 +1,11 @@ +{ + "type": "invoice", + "metadata": { + "type": "object", + "properties": { + "cost": { + "type": "number" + } + } + } +} diff --git a/tests/fixtures/audit_log_schema_target.json b/tests/fixtures/audit_log_schema_target.json new file mode 100644 index 00000000..328b5256 --- /dev/null +++ b/tests/fixtures/audit_log_schema_target.json @@ -0,0 +1,11 @@ +{ + "type": "invoice", + "metadata": { + "type": "object", + "properties": { + "cost": { + "type": "number" + } + } + } +} diff --git a/tests/fixtures/audit_logs_retention_json.json b/tests/fixtures/audit_logs_retention_json.json new file mode 100644 index 00000000..0720427e --- /dev/null +++ b/tests/fixtures/audit_logs_retention_json.json @@ -0,0 +1,3 @@ +{ + "retention_period_in_days": 30 +} diff --git a/tests/fixtures/authenticate_response.json b/tests/fixtures/authenticate_response.json new file mode 100644 index 00000000..5127ae3f --- /dev/null +++ b/tests/fixtures/authenticate_response.json @@ -0,0 +1,39 @@ +{ + "user": { + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": "Marcelina", + "last_name": "Davis", + "profile_picture_url": "https://workoscdn.com/images/v1/123abc", + "email": "marcelina.davis@example.com", + "email_verified": true, + "external_id": "f1ffa2b2-c20b-4d39-be5c-212726e11222", + "metadata": { + "timezone": "America/New_York" + }, + "last_sign_in_at": "2025-06-25T19:07:33.155Z", + "locale": "en-US", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + }, + "organization_id": "org_01H945H0YD4F97JN9MATX7BYAG", + "authkit_authorization_code": "authkit_authz_code_abc123", + "access_token": "eyJhb.nNzb19vaWRjX2tleV9.lc5Uk4yWVk5In0", + "refresh_token": "yAjhKk123NLIjdrBdGZPf8pLIDvK", + "authentication_method": "SSO", + "impersonator": { + "email": "admin@foocorp.com", + "reason": "Investigating an issue with the customer's account." + }, + "oauth_tokens": { + "provider": "GoogleOAuth", + "refresh_token": "1//04g...", + "access_token": "ya29.a0ARrdaM...", + "expires_at": 1735141800, + "scopes": [ + "profile", + "email", + "openid" + ] + } +} diff --git a/tests/fixtures/authenticate_response_impersonator.json b/tests/fixtures/authenticate_response_impersonator.json new file mode 100644 index 00000000..b483e3f0 --- /dev/null +++ b/tests/fixtures/authenticate_response_impersonator.json @@ -0,0 +1,4 @@ +{ + "email": "admin@foocorp.com", + "reason": "Investigating an issue with the customer's account." +} diff --git a/tests/fixtures/authenticate_response_oauth_token.json b/tests/fixtures/authenticate_response_oauth_token.json new file mode 100644 index 00000000..014b78c6 --- /dev/null +++ b/tests/fixtures/authenticate_response_oauth_token.json @@ -0,0 +1,11 @@ +{ + "provider": "GoogleOAuth", + "refresh_token": "1//04g...", + "access_token": "ya29.a0ARrdaM...", + "expires_at": 1735141800, + "scopes": [ + "profile", + "email", + "openid" + ] +} diff --git a/tests/fixtures/authentication_challenge.json b/tests/fixtures/authentication_challenge.json new file mode 100644 index 00000000..24e9becd --- /dev/null +++ b/tests/fixtures/authentication_challenge.json @@ -0,0 +1,9 @@ +{ + "object": "authentication_challenge", + "id": "auth_challenge_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "expires_at": "2026-01-15T12:00:00.000Z", + "code": "123456", + "authentication_factor_id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/authentication_challenge_verify_response.json b/tests/fixtures/authentication_challenge_verify_response.json new file mode 100644 index 00000000..e5f7ceca --- /dev/null +++ b/tests/fixtures/authentication_challenge_verify_response.json @@ -0,0 +1,12 @@ +{ + "challenge": { + "object": "authentication_challenge", + "id": "auth_challenge_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "expires_at": "2026-01-15T12:00:00.000Z", + "code": "123456", + "authentication_factor_id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + }, + "valid": true +} diff --git a/tests/fixtures/authentication_challenges_verify_request.json b/tests/fixtures/authentication_challenges_verify_request.json new file mode 100644 index 00000000..736bf40a --- /dev/null +++ b/tests/fixtures/authentication_challenges_verify_request.json @@ -0,0 +1,3 @@ +{ + "code": "123456" +} diff --git a/tests/fixtures/authentication_factor.json b/tests/fixtures/authentication_factor.json new file mode 100644 index 00000000..59d80419 --- /dev/null +++ b/tests/fixtures/authentication_factor.json @@ -0,0 +1,15 @@ +{ + "object": "authentication_factor", + "id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "type": "totp", + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "sms": { + "phone_number": "+15005550006" + }, + "totp": { + "issuer": "WorkOS", + "user": "user@example.com" + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/authentication_factor_enrolled.json b/tests/fixtures/authentication_factor_enrolled.json new file mode 100644 index 00000000..cf2b1cef --- /dev/null +++ b/tests/fixtures/authentication_factor_enrolled.json @@ -0,0 +1,18 @@ +{ + "object": "authentication_factor", + "id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "type": "totp", + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "sms": { + "phone_number": "+15005550006" + }, + "totp": { + "issuer": "WorkOS", + "user": "user@example.com", + "secret": "JBSWY3DPEHPK3PXP", + "qr_code": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...", + "uri": "otpauth://totp/WorkOS:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=WorkOS" + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/authentication_factor_enrolled_sms.json b/tests/fixtures/authentication_factor_enrolled_sms.json new file mode 100644 index 00000000..6b6f6880 --- /dev/null +++ b/tests/fixtures/authentication_factor_enrolled_sms.json @@ -0,0 +1,3 @@ +{ + "phone_number": "+15005550006" +} diff --git a/tests/fixtures/authentication_factor_enrolled_totp.json b/tests/fixtures/authentication_factor_enrolled_totp.json new file mode 100644 index 00000000..021cf1b6 --- /dev/null +++ b/tests/fixtures/authentication_factor_enrolled_totp.json @@ -0,0 +1,7 @@ +{ + "issuer": "WorkOS", + "user": "user@example.com", + "secret": "JBSWY3DPEHPK3PXP", + "qr_code": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...", + "uri": "otpauth://totp/WorkOS:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=WorkOS" +} diff --git a/tests/fixtures/authentication_factor_sms.json b/tests/fixtures/authentication_factor_sms.json new file mode 100644 index 00000000..6b6f6880 --- /dev/null +++ b/tests/fixtures/authentication_factor_sms.json @@ -0,0 +1,3 @@ +{ + "phone_number": "+15005550006" +} diff --git a/tests/fixtures/authentication_factor_totp.json b/tests/fixtures/authentication_factor_totp.json new file mode 100644 index 00000000..14ea6dc0 --- /dev/null +++ b/tests/fixtures/authentication_factor_totp.json @@ -0,0 +1,4 @@ +{ + "issuer": "WorkOS", + "user": "user@example.com" +} diff --git a/tests/fixtures/authentication_factors_create_request.json b/tests/fixtures/authentication_factors_create_request.json new file mode 100644 index 00000000..fdc9a408 --- /dev/null +++ b/tests/fixtures/authentication_factors_create_request.json @@ -0,0 +1,7 @@ +{ + "type": "totp", + "phone_number": "+15555555555", + "totp_issuer": "Foo Corp", + "totp_user": "alan.turing@example.com", + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5" +} diff --git a/tests/fixtures/authorization_check.json b/tests/fixtures/authorization_check.json new file mode 100644 index 00000000..8a5f5f97 --- /dev/null +++ b/tests/fixtures/authorization_check.json @@ -0,0 +1,3 @@ +{ + "authorized": true +} diff --git a/tests/fixtures/authorization_code_session_authenticate_request.json b/tests/fixtures/authorization_code_session_authenticate_request.json new file mode 100644 index 00000000..a68dbe87 --- /dev/null +++ b/tests/fixtures/authorization_code_session_authenticate_request.json @@ -0,0 +1,9 @@ +{ + "client_id": "client_01HXYZ123456789ABCDEFGHIJ", + "client_secret": "sk_test_....", + "grant_type": "authorization_code", + "code": "vBqZKaPpsnJlPfXiDqN7b6VTz", + "ip_address": "203.0.113.42", + "device_id": "device_01HXYZ123456789ABCDEFGHIJ", + "user_agent": "Mozilla/5.0" +} diff --git a/tests/fixtures/authorization_permission.json b/tests/fixtures/authorization_permission.json new file mode 100644 index 00000000..0424eed6 --- /dev/null +++ b/tests/fixtures/authorization_permission.json @@ -0,0 +1,11 @@ +{ + "object": "permission", + "id": "perm_01HXYZ123456789ABCDEFGHIJ", + "slug": "documents:read", + "name": "View Documents", + "description": "Allows viewing document contents", + "system": false, + "resource_type_slug": "workspace", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/authorization_resource.json b/tests/fixtures/authorization_resource.json new file mode 100644 index 00000000..ee5271ee --- /dev/null +++ b/tests/fixtures/authorization_resource.json @@ -0,0 +1,12 @@ +{ + "object": "authorization_resource", + "name": "Website Redesign", + "description": "Company website redesign project", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "parent_resource_id": "authz_resource_01HXYZ123456789ABCDEFGHIJ", + "id": "authz_resource_01HXYZ123456789ABCDEFGH", + "external_id": "proj-456", + "resource_type_slug": "project", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/authorized_connect_application_list_data.json b/tests/fixtures/authorized_connect_application_list_data.json new file mode 100644 index 00000000..0132aa02 --- /dev/null +++ b/tests/fixtures/authorized_connect_application_list_data.json @@ -0,0 +1,23 @@ +{ + "object": "authorized_connect_application", + "id": "aca_01HXYZ123456789ABCDEFGHIJ", + "granted_scopes": [ + "openid", + "profile", + "email" + ], + "application": { + "object": "connect_application", + "id": "conn_app_01HXYZ123456789ABCDEFGHIJ", + "client_id": "client_01HXYZ123456789ABCDEFGHIJ", + "description": "An application for managing user access", + "name": "My Application", + "scopes": [ + "openid", + "profile", + "email" + ], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } +} diff --git a/tests/fixtures/challenge_authentication_factor.json b/tests/fixtures/challenge_authentication_factor.json new file mode 100644 index 00000000..07c623af --- /dev/null +++ b/tests/fixtures/challenge_authentication_factor.json @@ -0,0 +1,3 @@ +{ + "sms_template": "Your verification code is {{code}}." +} diff --git a/tests/fixtures/check_authorization.json b/tests/fixtures/check_authorization.json new file mode 100644 index 00000000..ed8ae94e --- /dev/null +++ b/tests/fixtures/check_authorization.json @@ -0,0 +1,6 @@ +{ + "permission_slug": "posts:create", + "resource_id": "resource_01HXYZ123456789ABCDEFGHIJ", + "resource_external_id": "my-custom-id", + "resource_type_slug": "document" +} diff --git a/tests/fixtures/connect_application.json b/tests/fixtures/connect_application.json new file mode 100644 index 00000000..94258b88 --- /dev/null +++ b/tests/fixtures/connect_application.json @@ -0,0 +1,14 @@ +{ + "object": "connect_application", + "id": "conn_app_01HXYZ123456789ABCDEFGHIJ", + "client_id": "client_01HXYZ123456789ABCDEFGHIJ", + "description": "An application for managing user access", + "name": "My Application", + "scopes": [ + "openid", + "profile", + "email" + ], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/connected_account.json b/tests/fixtures/connected_account.json new file mode 100644 index 00000000..ffc4db77 --- /dev/null +++ b/tests/fixtures/connected_account.json @@ -0,0 +1,13 @@ +{ + "object": "connected_account", + "id": "data_installation_01EHZNVPK3SFK441A1RGBFSHRT", + "user_id": "user_01EHZNVPK3SFK441A1RGBFSHRT", + "organization_id": null, + "scopes": [ + "repo", + "user:email" + ], + "state": "connected", + "created_at": "2024-01-16T14:20:00.000Z", + "updated_at": "2024-01-16T14:20:00.000Z" +} diff --git a/tests/fixtures/connection.json b/tests/fixtures/connection.json new file mode 100644 index 00000000..2caf9273 --- /dev/null +++ b/tests/fixtures/connection.json @@ -0,0 +1,21 @@ +{ + "object": "connection", + "id": "conn_01E4ZCR3C56J083X43JQXF3JK5", + "organization_id": "org_01EHWNCE74X7JSDV0X3SZ3KJNY", + "connection_type": "OktaSAML", + "name": "Foo Corp", + "state": "active", + "status": "linked", + "domains": [ + { + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "object": "connection_domain", + "domain": "foo-corp.com" + } + ], + "options": { + "signing_cert": null + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/connection_domain.json b/tests/fixtures/connection_domain.json new file mode 100644 index 00000000..83396c2b --- /dev/null +++ b/tests/fixtures/connection_domain.json @@ -0,0 +1,5 @@ +{ + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "object": "connection_domain", + "domain": "foo-corp.com" +} diff --git a/tests/fixtures/connection_option.json b/tests/fixtures/connection_option.json new file mode 100644 index 00000000..3912dcb4 --- /dev/null +++ b/tests/fixtures/connection_option.json @@ -0,0 +1,3 @@ +{ + "signing_cert": null +} diff --git a/tests/fixtures/cors_origin_response.json b/tests/fixtures/cors_origin_response.json new file mode 100644 index 00000000..c70c3b3c --- /dev/null +++ b/tests/fixtures/cors_origin_response.json @@ -0,0 +1,7 @@ +{ + "object": "cors_origin", + "id": "cors_origin_01HXYZ123456789ABCDEFGHIJ", + "origin": "https://example.com", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/create_application_secret.json b/tests/fixtures/create_application_secret.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/tests/fixtures/create_application_secret.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/create_authorization_permission.json b/tests/fixtures/create_authorization_permission.json new file mode 100644 index 00000000..6c9dc2ae --- /dev/null +++ b/tests/fixtures/create_authorization_permission.json @@ -0,0 +1,6 @@ +{ + "slug": "documents:read", + "name": "View Documents", + "description": "Allows viewing document contents", + "resource_type_slug": "document" +} diff --git a/tests/fixtures/create_authorization_resource.json b/tests/fixtures/create_authorization_resource.json new file mode 100644 index 00000000..04b09a07 --- /dev/null +++ b/tests/fixtures/create_authorization_resource.json @@ -0,0 +1,10 @@ +{ + "external_id": "my-workspace-01", + "name": "Acme Workspace", + "description": "Primary workspace for the Acme team", + "resource_type_slug": "workspace", + "organization_id": "org_01EHQMYV6MBK39QC5PZXHY59C3", + "parent_resource_id": "authz_resource_01HXYZ123456789ABCDEFGHIJ", + "parent_resource_external_id": "parent-workspace-01", + "parent_resource_type_slug": "workspace" +} diff --git a/tests/fixtures/create_cors_origin.json b/tests/fixtures/create_cors_origin.json new file mode 100644 index 00000000..3b5477cf --- /dev/null +++ b/tests/fixtures/create_cors_origin.json @@ -0,0 +1,3 @@ +{ + "origin": "https://example.com" +} diff --git a/tests/fixtures/create_m2m_application.json b/tests/fixtures/create_m2m_application.json new file mode 100644 index 00000000..ecfaf00d --- /dev/null +++ b/tests/fixtures/create_m2m_application.json @@ -0,0 +1,11 @@ +{ + "name": "My Application", + "application_type": "m2m", + "description": "An application for managing user access", + "scopes": [ + "openid", + "profile", + "email" + ], + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT" +} diff --git a/tests/fixtures/create_magic_code_and_return.json b/tests/fixtures/create_magic_code_and_return.json new file mode 100644 index 00000000..10708580 --- /dev/null +++ b/tests/fixtures/create_magic_code_and_return.json @@ -0,0 +1,4 @@ +{ + "email": "marcelina.davis@example.com", + "invitation_token": "Z1Y2X3W4V5U6T7S8R9Q0P1O2N3" +} diff --git a/tests/fixtures/create_oauth_application.json b/tests/fixtures/create_oauth_application.json new file mode 100644 index 00000000..343693c1 --- /dev/null +++ b/tests/fixtures/create_oauth_application.json @@ -0,0 +1,19 @@ +{ + "name": "My Application", + "application_type": "oauth", + "description": "An application for managing user access", + "scopes": [ + "openid", + "profile", + "email" + ], + "redirect_uris": [ + { + "uri": "https://example.com/callback", + "default": true + } + ], + "uses_pkce": true, + "is_first_party": true, + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT" +} diff --git a/tests/fixtures/create_organization_api_key.json b/tests/fixtures/create_organization_api_key.json new file mode 100644 index 00000000..d2791c16 --- /dev/null +++ b/tests/fixtures/create_organization_api_key.json @@ -0,0 +1,7 @@ +{ + "name": "Production API Key", + "permissions": [ + "posts:read", + "posts:write" + ] +} diff --git a/tests/fixtures/create_organization_domain.json b/tests/fixtures/create_organization_domain.json new file mode 100644 index 00000000..ce43a5f2 --- /dev/null +++ b/tests/fixtures/create_organization_domain.json @@ -0,0 +1,4 @@ +{ + "domain": "foo-corp.com", + "organization_id": "org_01EHQMYV6MBK39QC5PZXHY59C3" +} diff --git a/tests/fixtures/create_organization_role.json b/tests/fixtures/create_organization_role.json new file mode 100644 index 00000000..6461264d --- /dev/null +++ b/tests/fixtures/create_organization_role.json @@ -0,0 +1,5 @@ +{ + "slug": "org-billing-admin", + "name": "Billing Administrator", + "description": "Can manage billing and invoices" +} diff --git a/tests/fixtures/create_password_reset.json b/tests/fixtures/create_password_reset.json new file mode 100644 index 00000000..d66c0ce0 --- /dev/null +++ b/tests/fixtures/create_password_reset.json @@ -0,0 +1,4 @@ +{ + "token": "Z1Y2X3W4V5U6T7S8R9Q0P1O2N3", + "new_password": "strong_password_123!" +} diff --git a/tests/fixtures/create_password_reset_token.json b/tests/fixtures/create_password_reset_token.json new file mode 100644 index 00000000..3cfe18e9 --- /dev/null +++ b/tests/fixtures/create_password_reset_token.json @@ -0,0 +1,3 @@ +{ + "email": "marcelina.davis@example.com" +} diff --git a/tests/fixtures/create_redirect_uri.json b/tests/fixtures/create_redirect_uri.json new file mode 100644 index 00000000..47e55d10 --- /dev/null +++ b/tests/fixtures/create_redirect_uri.json @@ -0,0 +1,3 @@ +{ + "uri": "https://example.com/callback" +} diff --git a/tests/fixtures/create_role.json b/tests/fixtures/create_role.json new file mode 100644 index 00000000..d3a2aadc --- /dev/null +++ b/tests/fixtures/create_role.json @@ -0,0 +1,6 @@ +{ + "slug": "editor", + "name": "Editor", + "description": "Can edit resources", + "resource_type_slug": "default" +} diff --git a/tests/fixtures/create_user.json b/tests/fixtures/create_user.json new file mode 100644 index 00000000..d64790ef --- /dev/null +++ b/tests/fixtures/create_user.json @@ -0,0 +1,13 @@ +{ + "email": "marcelina.davis@example.com", + "password": "strong_password_123!", + "password_hash": "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy", + "password_hash_type": "bcrypt", + "first_name": "Marcelina", + "last_name": "Davis", + "email_verified": true, + "metadata": { + "timezone": "America/New_York" + }, + "external_id": "f1ffa2b2-c20b-4d39-be5c-212726e11222" +} diff --git a/tests/fixtures/create_user_invite_options.json b/tests/fixtures/create_user_invite_options.json new file mode 100644 index 00000000..ba523b01 --- /dev/null +++ b/tests/fixtures/create_user_invite_options.json @@ -0,0 +1,8 @@ +{ + "email": "marcelina.davis@example.com", + "organization_id": "org_01E4ZCR3C56J083X43JQXF3JK5", + "role_slug": "admin", + "expires_in_days": 7, + "inviter_user_id": "user_01HYGBX8ZGD19949T3BM4FW1C3", + "locale": "en" +} diff --git a/tests/fixtures/create_user_organization_membership.json b/tests/fixtures/create_user_organization_membership.json new file mode 100644 index 00000000..b79b024e --- /dev/null +++ b/tests/fixtures/create_user_organization_membership.json @@ -0,0 +1,8 @@ +{ + "user_id": "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", + "organization_id": "org_01E4ZCR3C56J083X43JQXF3JK5", + "role_slug": "admin", + "role_slugs": [ + "admin" + ] +} diff --git a/tests/fixtures/create_webhook_endpoint.json b/tests/fixtures/create_webhook_endpoint.json new file mode 100644 index 00000000..c382a990 --- /dev/null +++ b/tests/fixtures/create_webhook_endpoint.json @@ -0,0 +1,7 @@ +{ + "endpoint_url": "https://example.com/webhooks", + "events": [ + "user.created", + "dsync.user.created" + ] +} diff --git a/tests/fixtures/data_integration_access_token_response.json b/tests/fixtures/data_integration_access_token_response.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/tests/fixtures/data_integration_access_token_response.json @@ -0,0 +1 @@ +{} diff --git a/tests/fixtures/data_integration_authorize_url_response.json b/tests/fixtures/data_integration_authorize_url_response.json new file mode 100644 index 00000000..0d6b120d --- /dev/null +++ b/tests/fixtures/data_integration_authorize_url_response.json @@ -0,0 +1,3 @@ +{ + "url": "https://api.workos.com/data-integrations/q2czJKmVAraSBg8xFpT7M9uR/authorize-redirect" +} diff --git a/tests/fixtures/data_integrations_get_data_integration_authorize_url_request.json b/tests/fixtures/data_integrations_get_data_integration_authorize_url_request.json new file mode 100644 index 00000000..b38a7cd5 --- /dev/null +++ b/tests/fixtures/data_integrations_get_data_integration_authorize_url_request.json @@ -0,0 +1,5 @@ +{ + "user_id": "user_01EHZNVPK3SFK441A1RGBFSHRT", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "return_to": "https://example.com/callback" +} diff --git a/tests/fixtures/data_integrations_get_user_token_request.json b/tests/fixtures/data_integrations_get_user_token_request.json new file mode 100644 index 00000000..ef8c4438 --- /dev/null +++ b/tests/fixtures/data_integrations_get_user_token_request.json @@ -0,0 +1,4 @@ +{ + "user_id": "user_01EHZNVPK3SFK441A1RGBFSHRT", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT" +} diff --git a/tests/fixtures/data_integrations_list_response.json b/tests/fixtures/data_integrations_list_response.json new file mode 100644 index 00000000..538e0843 --- /dev/null +++ b/tests/fixtures/data_integrations_list_response.json @@ -0,0 +1,35 @@ +{ + "object": "list", + "data": [ + { + "object": "data_provider", + "id": "data_integration_01EHZNVPK3SFK441A1RGBFSHRT", + "name": "GitHub", + "description": "Connect your GitHub account to access repositories.", + "slug": "github", + "integration_type": "github", + "credentials_type": "oauth2", + "scopes": [ + "repo", + "user:email" + ], + "ownership": "userland_user", + "created_at": "2024-01-15T10:30:00.000Z", + "updated_at": "2024-01-15T10:30:00.000Z", + "connected_account": { + "object": "connected_account", + "id": "data_installation_01EHZNVPK3SFK441A1RGBFSHRT", + "user_id": "user_01EHZNVPK3SFK441A1RGBFSHRT", + "organization_id": null, + "scopes": [ + "repo", + "user:email" + ], + "state": "connected", + "created_at": "2024-01-16T14:20:00.000Z", + "updated_at": "2024-01-16T14:20:00.000Z", + "userlandUserId": "test_userlandUserId" + } + } + ] +} diff --git a/tests/fixtures/data_integrations_list_response_data.json b/tests/fixtures/data_integrations_list_response_data.json new file mode 100644 index 00000000..5249b942 --- /dev/null +++ b/tests/fixtures/data_integrations_list_response_data.json @@ -0,0 +1,30 @@ +{ + "object": "data_provider", + "id": "data_integration_01EHZNVPK3SFK441A1RGBFSHRT", + "name": "GitHub", + "description": "Connect your GitHub account to access repositories.", + "slug": "github", + "integration_type": "github", + "credentials_type": "oauth2", + "scopes": [ + "repo", + "user:email" + ], + "ownership": "userland_user", + "created_at": "2024-01-15T10:30:00.000Z", + "updated_at": "2024-01-15T10:30:00.000Z", + "connected_account": { + "object": "connected_account", + "id": "data_installation_01EHZNVPK3SFK441A1RGBFSHRT", + "user_id": "user_01EHZNVPK3SFK441A1RGBFSHRT", + "organization_id": null, + "scopes": [ + "repo", + "user:email" + ], + "state": "connected", + "created_at": "2024-01-16T14:20:00.000Z", + "updated_at": "2024-01-16T14:20:00.000Z", + "userlandUserId": "test_userlandUserId" + } +} diff --git a/tests/fixtures/data_integrations_list_response_data_connected_account.json b/tests/fixtures/data_integrations_list_response_data_connected_account.json new file mode 100644 index 00000000..e79f3119 --- /dev/null +++ b/tests/fixtures/data_integrations_list_response_data_connected_account.json @@ -0,0 +1,14 @@ +{ + "object": "connected_account", + "id": "data_installation_01EHZNVPK3SFK441A1RGBFSHRT", + "user_id": "user_01EHZNVPK3SFK441A1RGBFSHRT", + "organization_id": null, + "scopes": [ + "repo", + "user:email" + ], + "state": "connected", + "created_at": "2024-01-16T14:20:00.000Z", + "updated_at": "2024-01-16T14:20:00.000Z", + "userlandUserId": "test_userlandUserId" +} diff --git a/tests/fixtures/device_authorization_response.json b/tests/fixtures/device_authorization_response.json new file mode 100644 index 00000000..465fca25 --- /dev/null +++ b/tests/fixtures/device_authorization_response.json @@ -0,0 +1,8 @@ +{ + "device_code": "CVE2wOfIFK4vhmiDBntpX9s8KT2f0qngpWYL0LGy9HxYgBRXUKIUkZB9BgIFho5h", + "user_code": "BCDF-GHJK", + "verification_uri": "https://authkit_domain/device", + "verification_uri_complete": "https://authkit_domain/device?user_code=BCDF-GHJK", + "expires_in": 300, + "interval": 5 +} diff --git a/tests/fixtures/directory.json b/tests/fixtures/directory.json new file mode 100644 index 00000000..e68f13cf --- /dev/null +++ b/tests/fixtures/directory.json @@ -0,0 +1,19 @@ +{ + "object": "directory", + "id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "external_key": "sPa12dwRQ", + "type": "gsuite directory", + "state": "unlinked", + "name": "Foo Corp", + "domain": "foo-corp.com", + "metadata": { + "users": { + "active": 42, + "inactive": 3 + }, + "groups": 5 + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/directory_group.json b/tests/fixtures/directory_group.json new file mode 100644 index 00000000..d4005ba4 --- /dev/null +++ b/tests/fixtures/directory_group.json @@ -0,0 +1,13 @@ +{ + "object": "directory_group", + "id": "directory_group_01E1JJS84MFPPQ3G655FHTKX6Z", + "idp_id": "02grqrue4294w24", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", + "name": "Developers", + "raw_attributes": { + "key": {} + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/directory_metadata.json b/tests/fixtures/directory_metadata.json new file mode 100644 index 00000000..0cfc2d4a --- /dev/null +++ b/tests/fixtures/directory_metadata.json @@ -0,0 +1,7 @@ +{ + "users": { + "active": 42, + "inactive": 3 + }, + "groups": 5 +} diff --git a/tests/fixtures/directory_metadata_user.json b/tests/fixtures/directory_metadata_user.json new file mode 100644 index 00000000..0c8d0ae7 --- /dev/null +++ b/tests/fixtures/directory_metadata_user.json @@ -0,0 +1,4 @@ +{ + "active": 42, + "inactive": 3 +} diff --git a/tests/fixtures/directory_user_with_groups.json b/tests/fixtures/directory_user_with_groups.json new file mode 100644 index 00000000..d7d35220 --- /dev/null +++ b/tests/fixtures/directory_user_with_groups.json @@ -0,0 +1,52 @@ +{ + "object": "directory_user", + "id": "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", + "idp_id": "2836", + "email": "marcelina.davis@example.com", + "first_name": "Marcelina", + "last_name": "Davis", + "emails": [ + { + "primary": true, + "type": "work", + "value": "marcelina.davis@example.com" + } + ], + "job_title": "Software Engineer", + "username": "mdavis", + "state": "active", + "raw_attributes": { + "key": {} + }, + "custom_attributes": { + "department": "Engineering", + "job_title": "Software Engineer" + }, + "role": { + "slug": "admin" + }, + "roles": [ + { + "slug": "admin" + } + ], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "groups": [ + { + "object": "directory_group", + "id": "directory_group_01E1JJS84MFPPQ3G655FHTKX6Z", + "idp_id": "02grqrue4294w24", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", + "name": "Developers", + "raw_attributes": { + "key": {} + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ] +} diff --git a/tests/fixtures/directory_user_with_groups_email.json b/tests/fixtures/directory_user_with_groups_email.json new file mode 100644 index 00000000..994ff49c --- /dev/null +++ b/tests/fixtures/directory_user_with_groups_email.json @@ -0,0 +1,5 @@ +{ + "primary": true, + "type": "work", + "value": "marcelina.davis@example.com" +} diff --git a/tests/fixtures/email_verification.json b/tests/fixtures/email_verification.json new file mode 100644 index 00000000..f77aa1db --- /dev/null +++ b/tests/fixtures/email_verification.json @@ -0,0 +1,10 @@ +{ + "object": "email_verification", + "id": "email_verification_01E4ZCR3C56J083X43JQXF3JK5", + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "expires_at": "2026-01-15T12:00:00.000Z", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "code": "123456" +} diff --git a/tests/fixtures/enroll_user_authentication_factor.json b/tests/fixtures/enroll_user_authentication_factor.json new file mode 100644 index 00000000..d6e24611 --- /dev/null +++ b/tests/fixtures/enroll_user_authentication_factor.json @@ -0,0 +1,6 @@ +{ + "type": "totp", + "totp_issuer": "WorkOS", + "totp_user": "user@example.com", + "totp_secret": "JBSWY3DPEHPK3PXP" +} diff --git a/tests/fixtures/event.json b/tests/fixtures/event.json new file mode 100644 index 00000000..119bae22 --- /dev/null +++ b/tests/fixtures/event.json @@ -0,0 +1,29 @@ +{ + "object": "event", + "id": "event_01EHZNVPK3SFK441A1RGBFSHRT", + "event": "dsync.user.created", + "data": { + "id": "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ", + "state": "active", + "emails": [ + { + "primary": true, + "value": "veda@foo-corp.com" + } + ], + "idp_id": "2836", + "object": "directory_user", + "username": "veda@foo-corp.com", + "last_name": "Torp", + "first_name": "Veda", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "raw_attributes": {}, + "custom_attributes": {}, + "created_at": "2021-06-25T19:07:33.155Z", + "updated_at": "2021-06-25T19:07:33.155Z" + }, + "created_at": "2026-01-15T12:00:00.000Z", + "context": { + "key": {} + } +} diff --git a/tests/fixtures/external_auth_complete_response.json b/tests/fixtures/external_auth_complete_response.json new file mode 100644 index 00000000..bda60f72 --- /dev/null +++ b/tests/fixtures/external_auth_complete_response.json @@ -0,0 +1,3 @@ +{ + "redirect_uri": "https://your-authkit-domain.workos.com/oauth/authorize/complete?state=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdGF0ZSI6InJhbmRvbV9zdGF0ZV9zdHJpbmciLCJpYXQiOjE3NDI2MDQ4NTN9.abc123def456ghi789" +} diff --git a/tests/fixtures/feature_flag.json b/tests/fixtures/feature_flag.json new file mode 100644 index 00000000..45acfc15 --- /dev/null +++ b/tests/fixtures/feature_flag.json @@ -0,0 +1,19 @@ +{ + "object": "feature_flag", + "id": "flag_01EHZNVPK3SFK441A1RGBFSHRT", + "slug": "advanced-analytics", + "name": "Advanced Analytics", + "description": "Enable advanced analytics dashboard feature", + "owner": { + "email": "jane@example.com", + "first_name": "Jane", + "last_name": "Doe" + }, + "tags": [ + "reports" + ], + "enabled": false, + "default_value": false, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/feature_flag_owner.json b/tests/fixtures/feature_flag_owner.json new file mode 100644 index 00000000..7c01c46f --- /dev/null +++ b/tests/fixtures/feature_flag_owner.json @@ -0,0 +1,5 @@ +{ + "email": "jane@example.com", + "first_name": "Jane", + "last_name": "Doe" +} diff --git a/tests/fixtures/flag.json b/tests/fixtures/flag.json new file mode 100644 index 00000000..7b79189a --- /dev/null +++ b/tests/fixtures/flag.json @@ -0,0 +1,19 @@ +{ + "object": "feature_flag", + "id": "flag_01EHZNVPK3SFK441A1RGBFSHRT", + "slug": "advanced-analytics", + "name": "Advanced Analytics", + "description": "Enable advanced analytics dashboard feature", + "owner": { + "email": "jane@example.com", + "first_name": "Jane", + "last_name": "Doe" + }, + "tags": [ + "reports" + ], + "enabled": true, + "default_value": false, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/flag_owner.json b/tests/fixtures/flag_owner.json new file mode 100644 index 00000000..7c01c46f --- /dev/null +++ b/tests/fixtures/flag_owner.json @@ -0,0 +1,5 @@ +{ + "email": "jane@example.com", + "first_name": "Jane", + "last_name": "Doe" +} diff --git a/tests/fixtures/generate_link.json b/tests/fixtures/generate_link.json new file mode 100644 index 00000000..f7fd7b0a --- /dev/null +++ b/tests/fixtures/generate_link.json @@ -0,0 +1,12 @@ +{ + "return_url": "https://example.com/admin-portal/return", + "success_url": "https://example.com/admin-portal/success", + "organization": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "intent": "sso", + "intent_options": { + "sso": { + "bookmark_slug": "chatgpt", + "provider_type": "GoogleSAML" + } + } +} diff --git a/tests/fixtures/intent_options.json b/tests/fixtures/intent_options.json new file mode 100644 index 00000000..31bad913 --- /dev/null +++ b/tests/fixtures/intent_options.json @@ -0,0 +1,6 @@ +{ + "sso": { + "bookmark_slug": "chatgpt", + "provider_type": "GoogleSAML" + } +} diff --git a/tests/fixtures/invitation.json b/tests/fixtures/invitation.json new file mode 100644 index 00000000..c2019df7 --- /dev/null +++ b/tests/fixtures/invitation.json @@ -0,0 +1,16 @@ +{ + "object": "invitation", + "id": "invitation_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "state": "accepted", + "accepted_at": "2026-01-15T12:00:00.000Z", + "revoked_at": null, + "expires_at": "2026-01-15T12:00:00.000Z", + "organization_id": "org_01E4ZCR3C56J083X43JQXF3JK5", + "inviter_user_id": "user_01HYGBX8ZGD19949T3BM4FW1C3", + "accepted_user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "token": "Z1uX3RbwcIl5fIGJJJCXXisdI", + "accept_invitation_url": "https://your-app.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI" +} diff --git a/tests/fixtures/jwks_response.json b/tests/fixtures/jwks_response.json new file mode 100644 index 00000000..2e865f38 --- /dev/null +++ b/tests/fixtures/jwks_response.json @@ -0,0 +1,16 @@ +{ + "keys": [ + { + "alg": "RS256", + "kty": "RSA", + "use": "sig", + "x5c": [ + "MIIDQjCCAiqgAwIBAgIGATz/FuLiMA0GCSqGSIb3DQEBCwUA..." + ], + "n": "0vx7agoebGc...eKnNs", + "e": "AQAB", + "kid": "key_01HXYZ123456789ABCDEFGHIJ", + "x5t#S256": "ZjQzYjI0OT...NmNjU0" + } + ] +} diff --git a/tests/fixtures/jwks_response_keys.json b/tests/fixtures/jwks_response_keys.json new file mode 100644 index 00000000..00560836 --- /dev/null +++ b/tests/fixtures/jwks_response_keys.json @@ -0,0 +1,12 @@ +{ + "alg": "RS256", + "kty": "RSA", + "use": "sig", + "x5c": [ + "MIIDQjCCAiqgAwIBAgIGATz/FuLiMA0GCSqGSIb3DQEBCwUA..." + ], + "n": "0vx7agoebGc...eKnNs", + "e": "AQAB", + "kid": "key_01HXYZ123456789ABCDEFGHIJ", + "x5t#S256": "ZjQzYjI0OT...NmNjU0" +} diff --git a/tests/fixtures/jwt_template_response.json b/tests/fixtures/jwt_template_response.json new file mode 100644 index 00000000..b691ce73 --- /dev/null +++ b/tests/fixtures/jwt_template_response.json @@ -0,0 +1,6 @@ +{ + "object": "jwt_template", + "content": "{\"iss\": \"{{environment.id}}\", \"sub\": \"{{user.id}}\"}", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/list.json b/tests/fixtures/list.json new file mode 100644 index 00000000..24e16799 --- /dev/null +++ b/tests/fixtures/list.json @@ -0,0 +1,20 @@ +{ + "object": "list", + "data": [ + { + "slug": "org-admin", + "object": "role", + "id": "role_01EHQMYV6MBK39QC5PZXHY59C3", + "name": "Organization Admin", + "description": "Can manage all resources", + "type": "OrganizationRole", + "resource_type_slug": "default", + "permissions": [ + "posts:read", + "posts:write" + ], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ] +} diff --git a/tests/fixtures/list_api_key.json b/tests/fixtures/list_api_key.json new file mode 100644 index 00000000..f6b9c99c --- /dev/null +++ b/tests/fixtures/list_api_key.json @@ -0,0 +1,25 @@ +{ + "data": [ + { + "object": "api_key", + "id": "api_key_01EHZNVPK3SFK441A1RGBFSHRT", + "owner": { + "type": "organization", + "id": "org_01EHZNVPK3SFK441A1RGBFSHRT" + }, + "name": "Production API Key", + "obfuscated_value": "sk_...3456", + "last_used_at": null, + "permissions": [ + "posts:read", + "posts:write" + ], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_audit_log_action_json.json b/tests/fixtures/list_audit_log_action_json.json new file mode 100644 index 00000000..f36c55df --- /dev/null +++ b/tests/fixtures/list_audit_log_action_json.json @@ -0,0 +1,50 @@ +{ + "data": [ + { + "object": "audit_log_action", + "name": "user.viewed_invoice", + "schema": { + "object": "audit_log_schema", + "version": 1, + "actor": { + "metadata": { + "type": "object", + "properties": { + "role": { + "type": "string" + } + } + } + }, + "targets": [ + { + "type": "invoice", + "metadata": { + "type": "object", + "properties": { + "cost": { + "type": "number" + } + } + } + } + ], + "metadata": { + "type": "object", + "properties": { + "transactionId": { + "type": "string" + } + } + }, + "created_at": "2026-01-15T12:00:00.000Z" + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_audit_log_schema_json.json b/tests/fixtures/list_audit_log_schema_json.json new file mode 100644 index 00000000..97bd6f5b --- /dev/null +++ b/tests/fixtures/list_audit_log_schema_json.json @@ -0,0 +1,44 @@ +{ + "data": [ + { + "object": "audit_log_schema", + "version": 1, + "actor": { + "metadata": { + "type": "object", + "properties": { + "role": { + "type": "string" + } + } + } + }, + "targets": [ + { + "type": "invoice", + "metadata": { + "type": "object", + "properties": { + "cost": { + "type": "number" + } + } + } + } + ], + "metadata": { + "type": "object", + "properties": { + "transactionId": { + "type": "string" + } + } + }, + "created_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_authentication_factor.json b/tests/fixtures/list_authentication_factor.json new file mode 100644 index 00000000..92ce3d94 --- /dev/null +++ b/tests/fixtures/list_authentication_factor.json @@ -0,0 +1,23 @@ +{ + "data": [ + { + "object": "authentication_factor", + "id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "type": "totp", + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "sms": { + "phone_number": "+15005550006" + }, + "totp": { + "issuer": "WorkOS", + "user": "user@example.com" + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_authorization_permission.json b/tests/fixtures/list_authorization_permission.json new file mode 100644 index 00000000..348f9d77 --- /dev/null +++ b/tests/fixtures/list_authorization_permission.json @@ -0,0 +1,19 @@ +{ + "data": [ + { + "object": "permission", + "id": "perm_01HXYZ123456789ABCDEFGHIJ", + "slug": "documents:read", + "name": "View Documents", + "description": "Allows viewing document contents", + "system": false, + "resource_type_slug": "workspace", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_authorization_resource.json b/tests/fixtures/list_authorization_resource.json new file mode 100644 index 00000000..1c184d8a --- /dev/null +++ b/tests/fixtures/list_authorization_resource.json @@ -0,0 +1,20 @@ +{ + "data": [ + { + "object": "authorization_resource", + "name": "Website Redesign", + "description": "Company website redesign project", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "parent_resource_id": "authz_resource_01HXYZ123456789ABCDEFGHIJ", + "id": "authz_resource_01HXYZ123456789ABCDEFGH", + "external_id": "proj-456", + "resource_type_slug": "project", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_authorized_connect_application_list_data.json b/tests/fixtures/list_authorized_connect_application_list_data.json new file mode 100644 index 00000000..198e9221 --- /dev/null +++ b/tests/fixtures/list_authorized_connect_application_list_data.json @@ -0,0 +1,31 @@ +{ + "data": [ + { + "object": "authorized_connect_application", + "id": "aca_01HXYZ123456789ABCDEFGHIJ", + "granted_scopes": [ + "openid", + "profile", + "email" + ], + "application": { + "object": "connect_application", + "id": "conn_app_01HXYZ123456789ABCDEFGHIJ", + "client_id": "client_01HXYZ123456789ABCDEFGHIJ", + "description": "An application for managing user access", + "name": "My Application", + "scopes": [ + "openid", + "profile", + "email" + ], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_connect_application.json b/tests/fixtures/list_connect_application.json new file mode 100644 index 00000000..52900bd8 --- /dev/null +++ b/tests/fixtures/list_connect_application.json @@ -0,0 +1,22 @@ +{ + "data": [ + { + "object": "connect_application", + "id": "conn_app_01HXYZ123456789ABCDEFGHIJ", + "client_id": "client_01HXYZ123456789ABCDEFGHIJ", + "description": "An application for managing user access", + "name": "My Application", + "scopes": [ + "openid", + "profile", + "email" + ], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_connection.json b/tests/fixtures/list_connection.json new file mode 100644 index 00000000..ace19723 --- /dev/null +++ b/tests/fixtures/list_connection.json @@ -0,0 +1,29 @@ +{ + "data": [ + { + "object": "connection", + "id": "conn_01E4ZCR3C56J083X43JQXF3JK5", + "organization_id": "org_01EHWNCE74X7JSDV0X3SZ3KJNY", + "connection_type": "OktaSAML", + "name": "Foo Corp", + "state": "active", + "status": "linked", + "domains": [ + { + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "object": "connection_domain", + "domain": "foo-corp.com" + } + ], + "options": { + "signing_cert": null + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_data.json b/tests/fixtures/list_data.json new file mode 100644 index 00000000..82458e1c --- /dev/null +++ b/tests/fixtures/list_data.json @@ -0,0 +1,15 @@ +{ + "slug": "org-admin", + "object": "role", + "id": "role_01EHQMYV6MBK39QC5PZXHY59C3", + "name": "Organization Admin", + "description": "Can manage all resources", + "type": "OrganizationRole", + "resource_type_slug": "default", + "permissions": [ + "posts:read", + "posts:write" + ], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/list_directory.json b/tests/fixtures/list_directory.json new file mode 100644 index 00000000..7b791a03 --- /dev/null +++ b/tests/fixtures/list_directory.json @@ -0,0 +1,27 @@ +{ + "data": [ + { + "object": "directory", + "id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "external_key": "sPa12dwRQ", + "type": "gsuite directory", + "state": "unlinked", + "name": "Foo Corp", + "domain": "foo-corp.com", + "metadata": { + "users": { + "active": 42, + "inactive": 3 + }, + "groups": 5 + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_directory_group.json b/tests/fixtures/list_directory_group.json new file mode 100644 index 00000000..d83487d5 --- /dev/null +++ b/tests/fixtures/list_directory_group.json @@ -0,0 +1,21 @@ +{ + "data": [ + { + "object": "directory_group", + "id": "directory_group_01E1JJS84MFPPQ3G655FHTKX6Z", + "idp_id": "02grqrue4294w24", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", + "name": "Developers", + "raw_attributes": { + "key": {} + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_directory_user_with_groups.json b/tests/fixtures/list_directory_user_with_groups.json new file mode 100644 index 00000000..58432bad --- /dev/null +++ b/tests/fixtures/list_directory_user_with_groups.json @@ -0,0 +1,60 @@ +{ + "data": [ + { + "object": "directory_user", + "id": "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", + "idp_id": "2836", + "email": "marcelina.davis@example.com", + "first_name": "Marcelina", + "last_name": "Davis", + "emails": [ + { + "primary": true, + "type": "work", + "value": "marcelina.davis@example.com" + } + ], + "job_title": "Software Engineer", + "username": "mdavis", + "state": "active", + "raw_attributes": { + "key": {} + }, + "custom_attributes": { + "department": "Engineering", + "job_title": "Software Engineer" + }, + "role": { + "slug": "admin" + }, + "roles": [ + { + "slug": "admin" + } + ], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "groups": [ + { + "object": "directory_group", + "id": "directory_group_01E1JJS84MFPPQ3G655FHTKX6Z", + "idp_id": "02grqrue4294w24", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", + "name": "Developers", + "raw_attributes": { + "key": {} + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ] + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_event.json b/tests/fixtures/list_event.json new file mode 100644 index 00000000..f44d25df --- /dev/null +++ b/tests/fixtures/list_event.json @@ -0,0 +1,37 @@ +{ + "data": [ + { + "object": "event", + "id": "event_01EHZNVPK3SFK441A1RGBFSHRT", + "event": "dsync.user.created", + "data": { + "id": "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ", + "state": "active", + "emails": [ + { + "primary": true, + "value": "veda@foo-corp.com" + } + ], + "idp_id": "2836", + "object": "directory_user", + "username": "veda@foo-corp.com", + "last_name": "Torp", + "first_name": "Veda", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "raw_attributes": {}, + "custom_attributes": {}, + "created_at": "2021-06-25T19:07:33.155Z", + "updated_at": "2021-06-25T19:07:33.155Z" + }, + "created_at": "2026-01-15T12:00:00.000Z", + "context": { + "key": {} + } + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_flag.json b/tests/fixtures/list_flag.json new file mode 100644 index 00000000..0b85ddbf --- /dev/null +++ b/tests/fixtures/list_flag.json @@ -0,0 +1,27 @@ +{ + "data": [ + { + "object": "feature_flag", + "id": "flag_01EHZNVPK3SFK441A1RGBFSHRT", + "slug": "advanced-analytics", + "name": "Advanced Analytics", + "description": "Enable advanced analytics dashboard feature", + "owner": { + "email": "jane@example.com", + "first_name": "Jane", + "last_name": "Doe" + }, + "tags": [ + "reports" + ], + "enabled": true, + "default_value": false, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_organization.json b/tests/fixtures/list_organization.json new file mode 100644 index 00000000..60eb366a --- /dev/null +++ b/tests/fixtures/list_organization.json @@ -0,0 +1,35 @@ +{ + "data": [ + { + "object": "organization", + "id": "org_01EHWNCE74X7JSDV0X3SZ3KJNY", + "name": "Acme Inc.", + "domains": [ + { + "object": "organization_domain", + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "organization_id": "org_01HE8GSH8FQPASKSY27THRKRBP", + "domain": "foo-corp.com", + "state": "pending", + "verification_prefix": "superapp-domain-verification-z3kjny", + "verification_token": "m5Oztg3jdK4NJLgs8uIlIprMw", + "verification_strategy": "dns", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "metadata": { + "tier": "diamond" + }, + "external_id": "2fe01467-f7ea-4dd2-8b79-c2b4f56d0191", + "stripe_customer_id": "cus_R9qWAGMQ6nGE7V", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "allow_profiles_outside_organization": false + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_role_assignment.json b/tests/fixtures/list_role_assignment.json new file mode 100644 index 00000000..81b4468c --- /dev/null +++ b/tests/fixtures/list_role_assignment.json @@ -0,0 +1,22 @@ +{ + "data": [ + { + "object": "role_assignment", + "id": "role_assignment_01HXYZ123456789ABCDEFGH", + "role": { + "slug": "admin" + }, + "resource": { + "id": "authz_resource_01HXYZ123456789ABCDEFGH", + "external_id": "proj-456", + "resource_type_slug": "project" + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_user.json b/tests/fixtures/list_user.json new file mode 100644 index 00000000..390c74f9 --- /dev/null +++ b/tests/fixtures/list_user.json @@ -0,0 +1,25 @@ +{ + "data": [ + { + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": "Marcelina", + "last_name": "Davis", + "profile_picture_url": "https://workoscdn.com/images/v1/123abc", + "email": "marcelina.davis@example.com", + "email_verified": true, + "external_id": "f1ffa2b2-c20b-4d39-be5c-212726e11222", + "metadata": { + "timezone": "America/New_York" + }, + "last_sign_in_at": "2025-06-25T19:07:33.155Z", + "locale": "en-US", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_user_invite.json b/tests/fixtures/list_user_invite.json new file mode 100644 index 00000000..355f3e44 --- /dev/null +++ b/tests/fixtures/list_user_invite.json @@ -0,0 +1,24 @@ +{ + "data": [ + { + "object": "invitation", + "id": "invitation_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "state": "pending", + "accepted_at": null, + "revoked_at": null, + "expires_at": "2026-01-15T12:00:00.000Z", + "organization_id": "org_01E4ZCR3C56J083X43JQXF3JK5", + "inviter_user_id": "user_01HYGBX8ZGD19949T3BM4FW1C3", + "accepted_user_id": null, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "token": "Z1uX3RbwcIl5fIGJJJCXXisdI", + "accept_invitation_url": "https://your-app.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_user_organization_membership.json b/tests/fixtures/list_user_organization_membership.json new file mode 100644 index 00000000..cdb823aa --- /dev/null +++ b/tests/fixtures/list_user_organization_membership.json @@ -0,0 +1,27 @@ +{ + "data": [ + { + "object": "organization_membership", + "id": "om_01HXYZ123456789ABCDEFGHIJ", + "user_id": "user_01EHQTV6MWP9P1F4ZXGXMC8ABB", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "status": "active", + "directory_managed": false, + "organization_name": "Acme Corp", + "custom_attributes": { + "department": "Engineering", + "title": "Developer Experience Engineer", + "location": "Brooklyn" + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "role": { + "slug": "admin" + } + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_user_organization_membership_base_list_data.json b/tests/fixtures/list_user_organization_membership_base_list_data.json new file mode 100644 index 00000000..b992a1b9 --- /dev/null +++ b/tests/fixtures/list_user_organization_membership_base_list_data.json @@ -0,0 +1,24 @@ +{ + "data": [ + { + "object": "organization_membership", + "id": "om_01HXYZ123456789ABCDEFGHIJ", + "user_id": "user_01EHQTV6MWP9P1F4ZXGXMC8ABB", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "status": "active", + "directory_managed": false, + "organization_name": "Acme Corp", + "custom_attributes": { + "department": "Engineering", + "title": "Developer Experience Engineer", + "location": "Brooklyn" + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_user_sessions_list_item.json b/tests/fixtures/list_user_sessions_list_item.json new file mode 100644 index 00000000..2da7cf43 --- /dev/null +++ b/tests/fixtures/list_user_sessions_list_item.json @@ -0,0 +1,26 @@ +{ + "data": [ + { + "object": "session", + "id": "session_01H93ZY4F80QPBEZ1R5B2SHQG8", + "impersonator": { + "email": "admin@foocorp.com", + "reason": "Investigating an issue with the customer's account." + }, + "ip_address": "198.51.100.42", + "organization_id": "org_01H945H0YD4F97JN9MATX7BYAG", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "auth_method": "sso", + "status": "active", + "expires_at": "2026-01-15T12:00:00.000Z", + "ended_at": null, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/list_webhook_endpoint_json.json b/tests/fixtures/list_webhook_endpoint_json.json new file mode 100644 index 00000000..52f8410b --- /dev/null +++ b/tests/fixtures/list_webhook_endpoint_json.json @@ -0,0 +1,21 @@ +{ + "data": [ + { + "object": "webhook_endpoint", + "id": "we_0123456789", + "endpoint_url": "https://example.com/webhooks", + "secret": "whsec_0FWAiVGkEfGBqqsJH4aNAGBJ4", + "status": "enabled", + "events": [ + "user.created", + "dsync.user.created" + ], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/fixtures/magic_auth.json b/tests/fixtures/magic_auth.json new file mode 100644 index 00000000..782a3e91 --- /dev/null +++ b/tests/fixtures/magic_auth.json @@ -0,0 +1,10 @@ +{ + "object": "magic_auth", + "id": "magic_auth_01HWZBQZY2M3AMQW166Q22K88F", + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "expires_at": "2026-01-15T12:00:00.000Z", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "code": "123456" +} diff --git a/tests/fixtures/new_connect_application_secret.json b/tests/fixtures/new_connect_application_secret.json new file mode 100644 index 00000000..9b772b11 --- /dev/null +++ b/tests/fixtures/new_connect_application_secret.json @@ -0,0 +1,9 @@ +{ + "object": "connect_application_secret", + "id": "secret_01J9Q2Z3X4Y5W6V7U8T9S0R1Q", + "secret_hint": "abc123", + "last_used_at": null, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "secret": "abc123def456ghi789jkl012mno345pqr678stu901vwx234yz" +} diff --git a/tests/fixtures/organization.json b/tests/fixtures/organization.json new file mode 100644 index 00000000..39eaa6fb --- /dev/null +++ b/tests/fixtures/organization.json @@ -0,0 +1,27 @@ +{ + "object": "organization", + "id": "org_01EHWNCE74X7JSDV0X3SZ3KJNY", + "name": "Acme Inc.", + "domains": [ + { + "object": "organization_domain", + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "organization_id": "org_01HE8GSH8FQPASKSY27THRKRBP", + "domain": "foo-corp.com", + "state": "pending", + "verification_prefix": "superapp-domain-verification-z3kjny", + "verification_token": "m5Oztg3jdK4NJLgs8uIlIprMw", + "verification_strategy": "dns", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ], + "metadata": { + "tier": "diamond" + }, + "external_id": "2fe01467-f7ea-4dd2-8b79-c2b4f56d0191", + "stripe_customer_id": "cus_R9qWAGMQ6nGE7V", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "allow_profiles_outside_organization": false +} diff --git a/tests/fixtures/organization_domain.json b/tests/fixtures/organization_domain.json new file mode 100644 index 00000000..9bc0c0a1 --- /dev/null +++ b/tests/fixtures/organization_domain.json @@ -0,0 +1,12 @@ +{ + "object": "organization_domain", + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "organization_id": "org_01HE8GSH8FQPASKSY27THRKRBP", + "domain": "foo-corp.com", + "state": "pending", + "verification_prefix": "superapp-domain-verification-z3kjny", + "verification_token": "m5Oztg3jdK4NJLgs8uIlIprMw", + "verification_strategy": "dns", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/organization_domain_data.json b/tests/fixtures/organization_domain_data.json new file mode 100644 index 00000000..f0032cb0 --- /dev/null +++ b/tests/fixtures/organization_domain_data.json @@ -0,0 +1,4 @@ +{ + "domain": "foo-corp.com", + "state": "verified" +} diff --git a/tests/fixtures/organization_domain_stand_alone.json b/tests/fixtures/organization_domain_stand_alone.json new file mode 100644 index 00000000..9bc0c0a1 --- /dev/null +++ b/tests/fixtures/organization_domain_stand_alone.json @@ -0,0 +1,12 @@ +{ + "object": "organization_domain", + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "organization_id": "org_01HE8GSH8FQPASKSY27THRKRBP", + "domain": "foo-corp.com", + "state": "pending", + "verification_prefix": "superapp-domain-verification-z3kjny", + "verification_token": "m5Oztg3jdK4NJLgs8uIlIprMw", + "verification_strategy": "dns", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/organization_dto.json b/tests/fixtures/organization_dto.json new file mode 100644 index 00000000..374fb083 --- /dev/null +++ b/tests/fixtures/organization_dto.json @@ -0,0 +1,17 @@ +{ + "name": "Foo Corp", + "allow_profiles_outside_organization": false, + "domains": [ + "example.com" + ], + "domain_data": [ + { + "domain": "foo-corp.com", + "state": "verified" + } + ], + "metadata": { + "tier": "diamond" + }, + "external_id": "ext_12345" +} diff --git a/tests/fixtures/organization_membership.json b/tests/fixtures/organization_membership.json new file mode 100644 index 00000000..afc05d1e --- /dev/null +++ b/tests/fixtures/organization_membership.json @@ -0,0 +1,19 @@ +{ + "object": "organization_membership", + "id": "om_01HXYZ123456789ABCDEFGHIJ", + "user_id": "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", + "organization_id": "org_01E4ZCR3C56J083X43JQXF3JK5", + "status": "active", + "directory_managed": false, + "organization_name": "Acme Corp", + "custom_attributes": { + "department": "Engineering", + "title": "Developer Experience Engineer", + "location": "Brooklyn" + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "role": { + "slug": "admin" + } +} diff --git a/tests/fixtures/password_reset.json b/tests/fixtures/password_reset.json new file mode 100644 index 00000000..d27b794f --- /dev/null +++ b/tests/fixtures/password_reset.json @@ -0,0 +1,10 @@ +{ + "object": "password_reset", + "id": "password_reset_01E4ZCR3C56J083X43JQXF3JK5", + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "expires_at": "2026-01-15T12:00:00.000Z", + "created_at": "2026-01-15T12:00:00.000Z", + "password_reset_token": "Z1uX3RbwcIl5fIGJJJCXXisdI", + "password_reset_url": "https://your-app.com/reset-password?token=Z1uX3RbwcIl5fIGJJJCXXisdI" +} diff --git a/tests/fixtures/password_session_authenticate_request.json b/tests/fixtures/password_session_authenticate_request.json new file mode 100644 index 00000000..ea240764 --- /dev/null +++ b/tests/fixtures/password_session_authenticate_request.json @@ -0,0 +1,11 @@ +{ + "client_id": "client_01HXYZ123456789ABCDEFGHIJ", + "client_secret": "sk_test_....", + "grant_type": "password", + "email": "user@example.com", + "password": "strong_password_123!", + "invitation_token": "inv_tok_01HXYZ123456789ABCDEFGHIJ", + "ip_address": "203.0.113.42", + "device_id": "device_01HXYZ123456789ABCDEFGHIJ", + "user_agent": "Mozilla/5.0" +} diff --git a/tests/fixtures/permission.json b/tests/fixtures/permission.json new file mode 100644 index 00000000..e4ac8b82 --- /dev/null +++ b/tests/fixtures/permission.json @@ -0,0 +1,11 @@ +{ + "object": "permission", + "id": "perm_01HXYZ123456789ABCDEFGHIJ", + "slug": "documents:read", + "name": "View Documents", + "description": "Allows viewing document contents", + "system": false, + "resource_type_slug": "document", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/portal_link_response.json b/tests/fixtures/portal_link_response.json new file mode 100644 index 00000000..811f32a9 --- /dev/null +++ b/tests/fixtures/portal_link_response.json @@ -0,0 +1,3 @@ +{ + "link": "https://setup.workos.com?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +} diff --git a/tests/fixtures/profile.json b/tests/fixtures/profile.json new file mode 100644 index 00000000..485a13ce --- /dev/null +++ b/tests/fixtures/profile.json @@ -0,0 +1,29 @@ +{ + "object": "profile", + "id": "prof_01DMC79VCBZ0NY2099737PSVF1", + "organization_id": "org_01EHQMYV6MBK39QC5PZXHY59C3", + "connection_id": "conn_01E4ZCR3C56J083X43JQXF3JK5", + "connection_type": "GoogleOAuth", + "idp_id": "103456789012345678901", + "email": "todd@example.com", + "first_name": "Todd", + "last_name": "Rundgren", + "role": { + "slug": "admin" + }, + "roles": [ + { + "slug": "admin" + } + ], + "groups": [ + "Engineering", + "Admins" + ], + "custom_attributes": { + "key": {} + }, + "raw_attributes": { + "key": {} + } +} diff --git a/tests/fixtures/radar_list_entry_already_present_response.json b/tests/fixtures/radar_list_entry_already_present_response.json new file mode 100644 index 00000000..d20390c3 --- /dev/null +++ b/tests/fixtures/radar_list_entry_already_present_response.json @@ -0,0 +1,3 @@ +{ + "message": "Entry already present in list" +} diff --git a/tests/fixtures/radar_standalone_assess_request.json b/tests/fixtures/radar_standalone_assess_request.json new file mode 100644 index 00000000..6f3673e8 --- /dev/null +++ b/tests/fixtures/radar_standalone_assess_request.json @@ -0,0 +1,9 @@ +{ + "ip_address": "49.78.240.97", + "user_agent": "Mozilla/5.0", + "email": "user@example.com", + "auth_method": "Password", + "action": "login", + "device_fingerprint": "fp_abc123", + "bot_score": "0.1" +} diff --git a/tests/fixtures/radar_standalone_delete_radar_list_entry_request.json b/tests/fixtures/radar_standalone_delete_radar_list_entry_request.json new file mode 100644 index 00000000..a123eb26 --- /dev/null +++ b/tests/fixtures/radar_standalone_delete_radar_list_entry_request.json @@ -0,0 +1,3 @@ +{ + "entry": "198.51.100.42" +} diff --git a/tests/fixtures/radar_standalone_response.json b/tests/fixtures/radar_standalone_response.json new file mode 100644 index 00000000..1670848e --- /dev/null +++ b/tests/fixtures/radar_standalone_response.json @@ -0,0 +1,7 @@ +{ + "verdict": "block", + "reason": "Detected enabled Radar control", + "attempt_id": "radar_att_01HZBC6N1EB1ZY7KG32X", + "control": "bot_detection", + "blocklist_type": "ip_address" +} diff --git a/tests/fixtures/radar_standalone_update_radar_attempt_request.json b/tests/fixtures/radar_standalone_update_radar_attempt_request.json new file mode 100644 index 00000000..b9598708 --- /dev/null +++ b/tests/fixtures/radar_standalone_update_radar_attempt_request.json @@ -0,0 +1,4 @@ +{ + "challenge_status": "success", + "attempt_status": "success" +} diff --git a/tests/fixtures/radar_standalone_update_radar_list_request.json b/tests/fixtures/radar_standalone_update_radar_list_request.json new file mode 100644 index 00000000..a123eb26 --- /dev/null +++ b/tests/fixtures/radar_standalone_update_radar_list_request.json @@ -0,0 +1,3 @@ +{ + "entry": "198.51.100.42" +} diff --git a/tests/fixtures/redirect_uri.json b/tests/fixtures/redirect_uri.json new file mode 100644 index 00000000..dcd6ad8d --- /dev/null +++ b/tests/fixtures/redirect_uri.json @@ -0,0 +1,8 @@ +{ + "object": "redirect_uri", + "id": "ruri_01EHZNVPK3SFK441A1RGBFSHRT", + "uri": "https://example.com/callback", + "default": true, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/redirect_uri_dto.json b/tests/fixtures/redirect_uri_dto.json new file mode 100644 index 00000000..083dd6a8 --- /dev/null +++ b/tests/fixtures/redirect_uri_dto.json @@ -0,0 +1,4 @@ +{ + "uri": "https://example.com/callback", + "default": true +} diff --git a/tests/fixtures/refresh_token_session_authenticate_request.json b/tests/fixtures/refresh_token_session_authenticate_request.json new file mode 100644 index 00000000..6acead44 --- /dev/null +++ b/tests/fixtures/refresh_token_session_authenticate_request.json @@ -0,0 +1,10 @@ +{ + "client_id": "client_01HXYZ123456789ABCDEFGHIJ", + "client_secret": "sk_test_....", + "grant_type": "refresh_token", + "refresh_token": "yAjhKk23hJMM3DaR...", + "organization_id": "org_01EHQMYV6MBK39QC5PZXHY59C3", + "ip_address": "203.0.113.42", + "device_id": "device_01HXYZ123456789ABCDEFGHIJ", + "user_agent": "Mozilla/5.0" +} diff --git a/tests/fixtures/remove_role.json b/tests/fixtures/remove_role.json new file mode 100644 index 00000000..f69fda37 --- /dev/null +++ b/tests/fixtures/remove_role.json @@ -0,0 +1,6 @@ +{ + "role_slug": "editor", + "resource_id": "authz_resource_01HXYZ123456789ABCDEFGH", + "resource_external_id": "external_01HXYZ123456789ABCDEFGH", + "resource_type_slug": "project" +} diff --git a/tests/fixtures/resend_user_invite_options.json b/tests/fixtures/resend_user_invite_options.json new file mode 100644 index 00000000..d160103c --- /dev/null +++ b/tests/fixtures/resend_user_invite_options.json @@ -0,0 +1,3 @@ +{ + "locale": "en" +} diff --git a/tests/fixtures/reset_password_response.json b/tests/fixtures/reset_password_response.json new file mode 100644 index 00000000..647d4c25 --- /dev/null +++ b/tests/fixtures/reset_password_response.json @@ -0,0 +1,19 @@ +{ + "user": { + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": "Marcelina", + "last_name": "Davis", + "profile_picture_url": "https://workoscdn.com/images/v1/123abc", + "email": "marcelina.davis@example.com", + "email_verified": true, + "external_id": "f1ffa2b2-c20b-4d39-be5c-212726e11222", + "metadata": { + "timezone": "America/New_York" + }, + "last_sign_in_at": "2025-06-25T19:07:33.155Z", + "locale": "en-US", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } +} diff --git a/tests/fixtures/revoke_session.json b/tests/fixtures/revoke_session.json new file mode 100644 index 00000000..c1d3e07f --- /dev/null +++ b/tests/fixtures/revoke_session.json @@ -0,0 +1,4 @@ +{ + "session_id": "session_01H93ZY4F80QPBEZ1R5B2SHQG8", + "return_to": "https://example.com" +} diff --git a/tests/fixtures/role.json b/tests/fixtures/role.json new file mode 100644 index 00000000..2e90a76a --- /dev/null +++ b/tests/fixtures/role.json @@ -0,0 +1,15 @@ +{ + "slug": "admin", + "object": "role", + "id": "role_01EHQMYV6MBK39QC5PZXHY59C3", + "name": "Admin", + "description": "Can manage all resources", + "type": "EnvironmentRole", + "resource_type_slug": "default", + "permissions": [ + "posts:read", + "posts:write" + ], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/role_assignment.json b/tests/fixtures/role_assignment.json new file mode 100644 index 00000000..4979ca1b --- /dev/null +++ b/tests/fixtures/role_assignment.json @@ -0,0 +1,14 @@ +{ + "object": "role_assignment", + "id": "role_assignment_01HXYZ123456789ABCDEFGH", + "role": { + "slug": "admin" + }, + "resource": { + "id": "authz_resource_01HXYZ123456789ABCDEFGH", + "external_id": "proj-456", + "resource_type_slug": "project" + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/role_assignment_resource.json b/tests/fixtures/role_assignment_resource.json new file mode 100644 index 00000000..53f9d508 --- /dev/null +++ b/tests/fixtures/role_assignment_resource.json @@ -0,0 +1,5 @@ +{ + "id": "authz_resource_01HXYZ123456789ABCDEFGH", + "external_id": "proj-456", + "resource_type_slug": "project" +} diff --git a/tests/fixtures/role_list.json b/tests/fixtures/role_list.json new file mode 100644 index 00000000..b2cb52c9 --- /dev/null +++ b/tests/fixtures/role_list.json @@ -0,0 +1,20 @@ +{ + "object": "list", + "data": [ + { + "slug": "admin", + "object": "role", + "id": "role_01EHQMYV6MBK39QC5PZXHY59C3", + "name": "Admin", + "description": "Can manage all resources", + "type": "EnvironmentRole", + "resource_type_slug": "default", + "permissions": [ + "posts:read", + "posts:write" + ], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } + ] +} diff --git a/tests/fixtures/send_verification_email_response.json b/tests/fixtures/send_verification_email_response.json new file mode 100644 index 00000000..647d4c25 --- /dev/null +++ b/tests/fixtures/send_verification_email_response.json @@ -0,0 +1,19 @@ +{ + "user": { + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": "Marcelina", + "last_name": "Davis", + "profile_picture_url": "https://workoscdn.com/images/v1/123abc", + "email": "marcelina.davis@example.com", + "email_verified": true, + "external_id": "f1ffa2b2-c20b-4d39-be5c-212726e11222", + "metadata": { + "timezone": "America/New_York" + }, + "last_sign_in_at": "2025-06-25T19:07:33.155Z", + "locale": "en-US", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } +} diff --git a/tests/fixtures/set_role_permissions.json b/tests/fixtures/set_role_permissions.json new file mode 100644 index 00000000..52910b36 --- /dev/null +++ b/tests/fixtures/set_role_permissions.json @@ -0,0 +1,8 @@ +{ + "permissions": [ + "billing:read", + "billing:write", + "invoices:manage", + "reports:view" + ] +} diff --git a/tests/fixtures/slim_role.json b/tests/fixtures/slim_role.json new file mode 100644 index 00000000..21f8f041 --- /dev/null +++ b/tests/fixtures/slim_role.json @@ -0,0 +1,3 @@ +{ + "slug": "admin" +} diff --git a/tests/fixtures/sso_authorize_url_response.json b/tests/fixtures/sso_authorize_url_response.json new file mode 100644 index 00000000..918b2b81 --- /dev/null +++ b/tests/fixtures/sso_authorize_url_response.json @@ -0,0 +1,3 @@ +{ + "url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=example&redirect_uri=https%3A%2F%2Fapi.workos.com%2Fsso%2Fcallback&response_type=code&scope=openid%20profile%20email" +} diff --git a/tests/fixtures/sso_device_authorization_request.json b/tests/fixtures/sso_device_authorization_request.json new file mode 100644 index 00000000..dffd0532 --- /dev/null +++ b/tests/fixtures/sso_device_authorization_request.json @@ -0,0 +1,3 @@ +{ + "client_id": "client_01HZBC6N1EB1ZY7KG32X" +} diff --git a/tests/fixtures/sso_intent_options.json b/tests/fixtures/sso_intent_options.json new file mode 100644 index 00000000..79ab315e --- /dev/null +++ b/tests/fixtures/sso_intent_options.json @@ -0,0 +1,4 @@ +{ + "bookmark_slug": "chatgpt", + "provider_type": "GoogleSAML" +} diff --git a/tests/fixtures/sso_logout_authorize_request.json b/tests/fixtures/sso_logout_authorize_request.json new file mode 100644 index 00000000..80a3c6e7 --- /dev/null +++ b/tests/fixtures/sso_logout_authorize_request.json @@ -0,0 +1,3 @@ +{ + "profile_id": "prof_01HXYZ123456789ABCDEFGHIJ" +} diff --git a/tests/fixtures/sso_logout_authorize_response.json b/tests/fixtures/sso_logout_authorize_response.json new file mode 100644 index 00000000..e4b79225 --- /dev/null +++ b/tests/fixtures/sso_logout_authorize_response.json @@ -0,0 +1,4 @@ +{ + "logout_url": "https://auth.workos.com/sso/logout?token=eyJhbGciOiJSUzI1NiJ9", + "logout_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9maWxlX2lkIjoicHJvZl8wMUdXUTFHMEgyRk02QVNFRjBIUzEzSENXOS0zMDRrZzAzZyIsImV4cCI6IjE1MTYyMzkwMjIifQ.Wru9Qlnf5DpohtGCKhZU4cVOd3zpiu7QQ-XEX--5A_4" +} diff --git a/tests/fixtures/sso_token_response.json b/tests/fixtures/sso_token_response.json new file mode 100644 index 00000000..757c3fb1 --- /dev/null +++ b/tests/fixtures/sso_token_response.json @@ -0,0 +1,45 @@ +{ + "token_type": "Bearer", + "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InNzby...", + "expires_in": 600, + "profile": { + "object": "profile", + "id": "prof_01DMC79VCBZ0NY2099737PSVF1", + "organization_id": "org_01EHQMYV6MBK39QC5PZXHY59C3", + "connection_id": "conn_01E4ZCR3C56J083X43JQXF3JK5", + "connection_type": "GoogleOAuth", + "idp_id": "103456789012345678901", + "email": "todd@example.com", + "first_name": "Todd", + "last_name": "Rundgren", + "role": { + "slug": "admin" + }, + "roles": [ + { + "slug": "admin" + } + ], + "groups": [ + "Engineering", + "Admins" + ], + "custom_attributes": { + "key": {} + }, + "raw_attributes": { + "key": {} + } + }, + "oauth_tokens": { + "provider": "GoogleOAuth", + "refresh_token": "1//04g...", + "access_token": "ya29.a0ARrdaM...", + "expires_at": 1735141800, + "scopes": [ + "profile", + "email", + "openid" + ] + } +} diff --git a/tests/fixtures/sso_token_response_oauth_token.json b/tests/fixtures/sso_token_response_oauth_token.json new file mode 100644 index 00000000..014b78c6 --- /dev/null +++ b/tests/fixtures/sso_token_response_oauth_token.json @@ -0,0 +1,11 @@ +{ + "provider": "GoogleOAuth", + "refresh_token": "1//04g...", + "access_token": "ya29.a0ARrdaM...", + "expires_at": 1735141800, + "scopes": [ + "profile", + "email", + "openid" + ] +} diff --git a/tests/fixtures/token_query.json b/tests/fixtures/token_query.json new file mode 100644 index 00000000..1051d0d8 --- /dev/null +++ b/tests/fixtures/token_query.json @@ -0,0 +1,6 @@ +{ + "client_id": "client_01HZBC6N1EB1ZY7KG32X", + "client_secret": "sk_example_123456789", + "code": "authorization_code_value", + "grant_type": "authorization_code" +} diff --git a/tests/fixtures/update_audit_logs_retention.json b/tests/fixtures/update_audit_logs_retention.json new file mode 100644 index 00000000..0720427e --- /dev/null +++ b/tests/fixtures/update_audit_logs_retention.json @@ -0,0 +1,3 @@ +{ + "retention_period_in_days": 30 +} diff --git a/tests/fixtures/update_authorization_permission.json b/tests/fixtures/update_authorization_permission.json new file mode 100644 index 00000000..5aed776b --- /dev/null +++ b/tests/fixtures/update_authorization_permission.json @@ -0,0 +1,4 @@ +{ + "name": "View Documents", + "description": "Allows viewing document contents" +} diff --git a/tests/fixtures/update_authorization_resource.json b/tests/fixtures/update_authorization_resource.json new file mode 100644 index 00000000..176a8025 --- /dev/null +++ b/tests/fixtures/update_authorization_resource.json @@ -0,0 +1,7 @@ +{ + "name": "Updated Name", + "description": "Updated description", + "parent_resource_id": "authz_resource_01HXYZ123456789ABCDEFGHIJ", + "parent_resource_external_id": "parent-workspace-01", + "parent_resource_type_slug": "workspace" +} diff --git a/tests/fixtures/update_jwt_template.json b/tests/fixtures/update_jwt_template.json new file mode 100644 index 00000000..bb613667 --- /dev/null +++ b/tests/fixtures/update_jwt_template.json @@ -0,0 +1,3 @@ +{ + "content": "{\"iss\": \"{{environment.id}}\", \"sub\": \"{{user.id}}\"}" +} diff --git a/tests/fixtures/update_oauth_application.json b/tests/fixtures/update_oauth_application.json new file mode 100644 index 00000000..24005a39 --- /dev/null +++ b/tests/fixtures/update_oauth_application.json @@ -0,0 +1,15 @@ +{ + "name": "My Application", + "description": "An application for managing user access", + "scopes": [ + "openid", + "profile", + "email" + ], + "redirect_uris": [ + { + "uri": "https://example.com/callback", + "default": true + } + ] +} diff --git a/tests/fixtures/update_organization.json b/tests/fixtures/update_organization.json new file mode 100644 index 00000000..3e74efde --- /dev/null +++ b/tests/fixtures/update_organization.json @@ -0,0 +1,18 @@ +{ + "name": "Foo Corp", + "allow_profiles_outside_organization": false, + "domains": [ + "foo-corp.com" + ], + "domain_data": [ + { + "domain": "foo-corp.com", + "state": "verified" + } + ], + "stripe_customer_id": "cus_R9qWAGMQ6nGE7V", + "metadata": { + "tier": "diamond" + }, + "external_id": "2fe01467-f7ea-4dd2-8b79-c2b4f56d0191" +} diff --git a/tests/fixtures/update_organization_role.json b/tests/fixtures/update_organization_role.json new file mode 100644 index 00000000..416ea3d1 --- /dev/null +++ b/tests/fixtures/update_organization_role.json @@ -0,0 +1,4 @@ +{ + "name": "Finance Administrator", + "description": "Can manage all financial operations" +} diff --git a/tests/fixtures/update_role.json b/tests/fixtures/update_role.json new file mode 100644 index 00000000..339fb5b5 --- /dev/null +++ b/tests/fixtures/update_role.json @@ -0,0 +1,4 @@ +{ + "name": "Super Administrator", + "description": "Full administrative access to all resources" +} diff --git a/tests/fixtures/update_user.json b/tests/fixtures/update_user.json new file mode 100644 index 00000000..7242be17 --- /dev/null +++ b/tests/fixtures/update_user.json @@ -0,0 +1,14 @@ +{ + "email": "marcelina.davis@example.com", + "first_name": "Marcelina", + "last_name": "Davis", + "email_verified": true, + "password": "strong_password_123!", + "password_hash": "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy", + "password_hash_type": "bcrypt", + "metadata": { + "timezone": "America/New_York" + }, + "external_id": "f1ffa2b2-c20b-4d39-be5c-212726e11222", + "locale": "en-US" +} diff --git a/tests/fixtures/update_user_organization_membership.json b/tests/fixtures/update_user_organization_membership.json new file mode 100644 index 00000000..d76ea5c3 --- /dev/null +++ b/tests/fixtures/update_user_organization_membership.json @@ -0,0 +1,6 @@ +{ + "role_slug": "admin", + "role_slugs": [ + "admin" + ] +} diff --git a/tests/fixtures/update_webhook_endpoint.json b/tests/fixtures/update_webhook_endpoint.json new file mode 100644 index 00000000..7bfe7e33 --- /dev/null +++ b/tests/fixtures/update_webhook_endpoint.json @@ -0,0 +1,8 @@ +{ + "endpoint_url": "https://example.com/webhooks", + "status": "enabled", + "events": [ + "user.created", + "dsync.user.created" + ] +} diff --git a/tests/fixtures/urn_ietf_params_oauth_grant_type_device_code_session_authenticate_request.json b/tests/fixtures/urn_ietf_params_oauth_grant_type_device_code_session_authenticate_request.json new file mode 100644 index 00000000..fd6c75f7 --- /dev/null +++ b/tests/fixtures/urn_ietf_params_oauth_grant_type_device_code_session_authenticate_request.json @@ -0,0 +1,8 @@ +{ + "client_id": "client_01HXYZ123456789ABCDEFGHIJ", + "grant_type": "urn:ietf:params:oauth:grant-type:device_code", + "device_code": "Ao4fMrDS...", + "ip_address": "203.0.113.42", + "device_id": "device_01HXYZ123456789ABCDEFGHIJ", + "user_agent": "Mozilla/5.0" +} diff --git a/tests/fixtures/urn_workos_oauth_grant_type_email_verification_code_session_authenticate_request.json b/tests/fixtures/urn_workos_oauth_grant_type_email_verification_code_session_authenticate_request.json new file mode 100644 index 00000000..47ad4f73 --- /dev/null +++ b/tests/fixtures/urn_workos_oauth_grant_type_email_verification_code_session_authenticate_request.json @@ -0,0 +1,10 @@ +{ + "client_id": "client_01HXYZ123456789ABCDEFGHIJ", + "client_secret": "sk_test_....", + "grant_type": "urn:workos:oauth:grant-type:email-verification:code", + "code": "123456", + "pending_authentication_token": "cTDQJTTkTkkVYxbn...", + "ip_address": "203.0.113.42", + "device_id": "device_01HXYZ123456789ABCDEFGHIJ", + "user_agent": "Mozilla/5.0" +} diff --git a/tests/fixtures/urn_workos_oauth_grant_type_magic_auth_code_session_authenticate_request.json b/tests/fixtures/urn_workos_oauth_grant_type_magic_auth_code_session_authenticate_request.json new file mode 100644 index 00000000..ebd5511b --- /dev/null +++ b/tests/fixtures/urn_workos_oauth_grant_type_magic_auth_code_session_authenticate_request.json @@ -0,0 +1,11 @@ +{ + "client_id": "client_01HXYZ123456789ABCDEFGHIJ", + "client_secret": "sk_test_....", + "grant_type": "urn:workos:oauth:grant-type:magic-auth:code", + "code": "123456", + "email": "user@example.com", + "invitation_token": "inv_tok_01HXYZ123456789ABCDEFGHIJ", + "ip_address": "203.0.113.42", + "device_id": "device_01HXYZ123456789ABCDEFGHIJ", + "user_agent": "Mozilla/5.0" +} diff --git a/tests/fixtures/urn_workos_oauth_grant_type_mfa_totp_session_authenticate_request.json b/tests/fixtures/urn_workos_oauth_grant_type_mfa_totp_session_authenticate_request.json new file mode 100644 index 00000000..31382557 --- /dev/null +++ b/tests/fixtures/urn_workos_oauth_grant_type_mfa_totp_session_authenticate_request.json @@ -0,0 +1,11 @@ +{ + "client_id": "client_01HXYZ123456789ABCDEFGHIJ", + "client_secret": "sk_test_....", + "grant_type": "urn:workos:oauth:grant-type:mfa-totp", + "code": "123456", + "pending_authentication_token": "cTDQJTTkTkkVYxbn...", + "authentication_challenge_id": "auth_challenge_01HXYZ123456789ABCDEFGHIJ", + "ip_address": "203.0.113.42", + "device_id": "device_01HXYZ123456789ABCDEFGHIJ", + "user_agent": "Mozilla/5.0" +} diff --git a/tests/fixtures/urn_workos_oauth_grant_type_organization_selection_session_authenticate_request.json b/tests/fixtures/urn_workos_oauth_grant_type_organization_selection_session_authenticate_request.json new file mode 100644 index 00000000..7d0fb2f3 --- /dev/null +++ b/tests/fixtures/urn_workos_oauth_grant_type_organization_selection_session_authenticate_request.json @@ -0,0 +1,10 @@ +{ + "client_id": "client_01HXYZ123456789ABCDEFGHIJ", + "client_secret": "sk_test_....", + "grant_type": "urn:workos:oauth:grant-type:organization-selection", + "pending_authentication_token": "cTDQJTTkTkkVYxbn...", + "organization_id": "org_01EHQMYV6MBK39QC5PZXHY59C3", + "ip_address": "203.0.113.42", + "device_id": "device_01HXYZ123456789ABCDEFGHIJ", + "user_agent": "Mozilla/5.0" +} diff --git a/tests/fixtures/user.json b/tests/fixtures/user.json new file mode 100644 index 00000000..ea8be141 --- /dev/null +++ b/tests/fixtures/user.json @@ -0,0 +1,17 @@ +{ + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": "Marcelina", + "last_name": "Davis", + "profile_picture_url": "https://workoscdn.com/images/v1/123abc", + "email": "marcelina.davis@example.com", + "email_verified": true, + "external_id": "f1ffa2b2-c20b-4d39-be5c-212726e11222", + "metadata": { + "timezone": "America/New_York" + }, + "last_sign_in_at": "2025-06-25T19:07:33.155Z", + "locale": "en-US", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/user_authentication_factor_enroll_response.json b/tests/fixtures/user_authentication_factor_enroll_response.json new file mode 100644 index 00000000..49b9187a --- /dev/null +++ b/tests/fixtures/user_authentication_factor_enroll_response.json @@ -0,0 +1,29 @@ +{ + "authentication_factor": { + "object": "authentication_factor", + "id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "type": "totp", + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "sms": { + "phone_number": "+15005550006" + }, + "totp": { + "issuer": "WorkOS", + "user": "user@example.com", + "secret": "JBSWY3DPEHPK3PXP", + "qr_code": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...", + "uri": "otpauth://totp/WorkOS:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=WorkOS" + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + }, + "authentication_challenge": { + "object": "authentication_challenge", + "id": "auth_challenge_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "expires_at": "2026-01-15T12:00:00.000Z", + "code": "123456", + "authentication_factor_id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } +} diff --git a/tests/fixtures/user_consent_option.json b/tests/fixtures/user_consent_option.json new file mode 100644 index 00000000..ba21c619 --- /dev/null +++ b/tests/fixtures/user_consent_option.json @@ -0,0 +1,11 @@ +{ + "claim": "tos_accepted", + "type": "enum", + "label": "Terms of Service", + "choices": [ + { + "value": "accepted", + "label": "I accept the Terms of Service" + } + ] +} diff --git a/tests/fixtures/user_consent_option_choice.json b/tests/fixtures/user_consent_option_choice.json new file mode 100644 index 00000000..c5f1f838 --- /dev/null +++ b/tests/fixtures/user_consent_option_choice.json @@ -0,0 +1,4 @@ +{ + "value": "accepted", + "label": "I accept the Terms of Service" +} diff --git a/tests/fixtures/user_identities_get_item.json b/tests/fixtures/user_identities_get_item.json new file mode 100644 index 00000000..46a77e2e --- /dev/null +++ b/tests/fixtures/user_identities_get_item.json @@ -0,0 +1,5 @@ +{ + "idp_id": "4F42ABDE-1E44-4B66-824A-5F733C037A6D", + "type": "OAuth", + "provider": "MicrosoftOAuth" +} diff --git a/tests/fixtures/user_invite.json b/tests/fixtures/user_invite.json new file mode 100644 index 00000000..b3a1dbbd --- /dev/null +++ b/tests/fixtures/user_invite.json @@ -0,0 +1,16 @@ +{ + "object": "invitation", + "id": "invitation_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "state": "pending", + "accepted_at": null, + "revoked_at": null, + "expires_at": "2026-01-15T12:00:00.000Z", + "organization_id": "org_01E4ZCR3C56J083X43JQXF3JK5", + "inviter_user_id": "user_01HYGBX8ZGD19949T3BM4FW1C3", + "accepted_user_id": null, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "token": "Z1uX3RbwcIl5fIGJJJCXXisdI", + "accept_invitation_url": "https://your-app.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI" +} diff --git a/tests/fixtures/user_management_login_request.json b/tests/fixtures/user_management_login_request.json new file mode 100644 index 00000000..952fbbf4 --- /dev/null +++ b/tests/fixtures/user_management_login_request.json @@ -0,0 +1,26 @@ +{ + "external_auth_id": "ext_auth_01HXYZ123456789ABCDEFGHIJ", + "user": { + "id": "user_12345", + "email": "marcelina.davis@example.com", + "first_name": "Marcelina", + "last_name": "Davis", + "metadata": { + "department": "Engineering", + "role": "Developer" + } + }, + "user_consent_options": [ + { + "claim": "tos_accepted", + "type": "enum", + "label": "Terms of Service", + "choices": [ + { + "value": "accepted", + "label": "I accept the Terms of Service" + } + ] + } + ] +} diff --git a/tests/fixtures/user_object.json b/tests/fixtures/user_object.json new file mode 100644 index 00000000..27b814fd --- /dev/null +++ b/tests/fixtures/user_object.json @@ -0,0 +1,10 @@ +{ + "id": "user_12345", + "email": "marcelina.davis@example.com", + "first_name": "Marcelina", + "last_name": "Davis", + "metadata": { + "department": "Engineering", + "role": "Developer" + } +} diff --git a/tests/fixtures/user_organization_membership.json b/tests/fixtures/user_organization_membership.json new file mode 100644 index 00000000..d6acfbe6 --- /dev/null +++ b/tests/fixtures/user_organization_membership.json @@ -0,0 +1,19 @@ +{ + "object": "organization_membership", + "id": "om_01HXYZ123456789ABCDEFGHIJ", + "user_id": "user_01EHQTV6MWP9P1F4ZXGXMC8ABB", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "status": "active", + "directory_managed": false, + "organization_name": "Acme Corp", + "custom_attributes": { + "department": "Engineering", + "title": "Developer Experience Engineer", + "location": "Brooklyn" + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "role": { + "slug": "admin" + } +} diff --git a/tests/fixtures/user_organization_membership_base_list_data.json b/tests/fixtures/user_organization_membership_base_list_data.json new file mode 100644 index 00000000..a4e9ebde --- /dev/null +++ b/tests/fixtures/user_organization_membership_base_list_data.json @@ -0,0 +1,16 @@ +{ + "object": "organization_membership", + "id": "om_01HXYZ123456789ABCDEFGHIJ", + "user_id": "user_01EHQTV6MWP9P1F4ZXGXMC8ABB", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "status": "active", + "directory_managed": false, + "organization_name": "Acme Corp", + "custom_attributes": { + "department": "Engineering", + "title": "Developer Experience Engineer", + "location": "Brooklyn" + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/user_sessions_impersonator.json b/tests/fixtures/user_sessions_impersonator.json new file mode 100644 index 00000000..b483e3f0 --- /dev/null +++ b/tests/fixtures/user_sessions_impersonator.json @@ -0,0 +1,4 @@ +{ + "email": "admin@foocorp.com", + "reason": "Investigating an issue with the customer's account." +} diff --git a/tests/fixtures/user_sessions_list_item.json b/tests/fixtures/user_sessions_list_item.json new file mode 100644 index 00000000..233ec56a --- /dev/null +++ b/tests/fixtures/user_sessions_list_item.json @@ -0,0 +1,18 @@ +{ + "object": "session", + "id": "session_01H93ZY4F80QPBEZ1R5B2SHQG8", + "impersonator": { + "email": "admin@foocorp.com", + "reason": "Investigating an issue with the customer's account." + }, + "ip_address": "198.51.100.42", + "organization_id": "org_01H945H0YD4F97JN9MATX7BYAG", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "auth_method": "sso", + "status": "active", + "expires_at": "2026-01-15T12:00:00.000Z", + "ended_at": null, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/validate_api_key.json b/tests/fixtures/validate_api_key.json new file mode 100644 index 00000000..d859ae78 --- /dev/null +++ b/tests/fixtures/validate_api_key.json @@ -0,0 +1,3 @@ +{ + "value": "sk_example_1234567890abcdef" +} diff --git a/tests/fixtures/verify_email_address.json b/tests/fixtures/verify_email_address.json new file mode 100644 index 00000000..736bf40a --- /dev/null +++ b/tests/fixtures/verify_email_address.json @@ -0,0 +1,3 @@ +{ + "code": "123456" +} diff --git a/tests/fixtures/verify_email_response.json b/tests/fixtures/verify_email_response.json new file mode 100644 index 00000000..647d4c25 --- /dev/null +++ b/tests/fixtures/verify_email_response.json @@ -0,0 +1,19 @@ +{ + "user": { + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": "Marcelina", + "last_name": "Davis", + "profile_picture_url": "https://workoscdn.com/images/v1/123abc", + "email": "marcelina.davis@example.com", + "email_verified": true, + "external_id": "f1ffa2b2-c20b-4d39-be5c-212726e11222", + "metadata": { + "timezone": "America/New_York" + }, + "last_sign_in_at": "2025-06-25T19:07:33.155Z", + "locale": "en-US", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" + } +} diff --git a/tests/fixtures/webhook_endpoint_json.json b/tests/fixtures/webhook_endpoint_json.json new file mode 100644 index 00000000..3f37fa02 --- /dev/null +++ b/tests/fixtures/webhook_endpoint_json.json @@ -0,0 +1,13 @@ +{ + "object": "webhook_endpoint", + "id": "we_0123456789", + "endpoint_url": "https://example.com/webhooks", + "secret": "whsec_0FWAiVGkEfGBqqsJH4aNAGBJ4", + "status": "enabled", + "events": [ + "user.created", + "dsync.user.created" + ], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z" +} diff --git a/tests/fixtures/widget_session_token.json b/tests/fixtures/widget_session_token.json new file mode 100644 index 00000000..0f0c903a --- /dev/null +++ b/tests/fixtures/widget_session_token.json @@ -0,0 +1,7 @@ +{ + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "scopes": [ + "widgets:users-table:manage" + ] +} diff --git a/tests/fixtures/widget_session_token_response.json b/tests/fixtures/widget_session_token_response.json new file mode 100644 index 00000000..0a78c2fd --- /dev/null +++ b/tests/fixtures/widget_session_token_response.json @@ -0,0 +1,3 @@ +{ + "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InNlc3Npb24..." +} diff --git a/tests/generated_helpers.py b/tests/generated_helpers.py new file mode 100644 index 00000000..6ed54765 --- /dev/null +++ b/tests/generated_helpers.py @@ -0,0 +1,14 @@ +# This file is auto-generated by oagen. Do not edit. + +import json +import os + + +FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixtures") + + +def load_fixture(name: str) -> dict: + """Load a JSON fixture file by name.""" + path = os.path.join(FIXTURES_DIR, name) + with open(path) as f: + return json.load(f) diff --git a/tests/test_admin_portal.py b/tests/test_admin_portal.py new file mode 100644 index 00000000..9eac10d6 --- /dev/null +++ b/tests/test_admin_portal.py @@ -0,0 +1,131 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.admin_portal.models import PortalLinkResponse +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestAdminPortal: + def test_create(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("portal_link_response.json"), + ) + result = workos.admin_portal.create(organization="test_organization") + assert isinstance(result, PortalLinkResponse) + assert ( + result.link + == "https://setup.workos.com?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + ) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/portal/generate_link") + body = json.loads(request.content) + assert body["organization"] == "test_organization" + + def test_create_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.admin_portal.create(organization="test_organization") + + def test_create_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.admin_portal.create(organization="test_organization") + finally: + workos.close() + + def test_create_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.admin_portal.create(organization="test_organization") + finally: + workos.close() + + def test_create_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.admin_portal.create(organization="test_organization") + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncAdminPortal: + async def test_create(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("portal_link_response.json")) + result = await async_workos.admin_portal.create( + organization="test_organization" + ) + assert isinstance(result, PortalLinkResponse) + assert ( + result.link + == "https://setup.workos.com?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + ) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/portal/generate_link") + + async def test_create_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.admin_portal.create(organization="test_organization") + + async def test_create_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.admin_portal.create(organization="test_organization") + finally: + await workos.close() + + async def test_create_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.admin_portal.create(organization="test_organization") + finally: + await workos.close() + + async def test_create_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.admin_portal.create(organization="test_organization") + finally: + await workos.close() diff --git a/tests/test_api_keys.py b/tests/test_api_keys.py new file mode 100644 index 00000000..0d03277b --- /dev/null +++ b/tests/test_api_keys.py @@ -0,0 +1,137 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.api_keys.models import ApiKeyValidationResponse +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestApiKeys: + def test_validate_api_key(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("api_key_validation_response.json"), + ) + result = workos.api_keys.validate_api_key(value="test_value") + assert isinstance(result, ApiKeyValidationResponse) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/api_keys/validations") + body = json.loads(request.content) + assert body["value"] == "test_value" + + def test_delete_api_key(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.api_keys.delete_api_key("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/api_keys/test_id") + + def test_validate_api_key_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.api_keys.validate_api_key(value="test_value") + + def test_validate_api_key_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.api_keys.validate_api_key(value="test_value") + finally: + workos.close() + + def test_validate_api_key_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.api_keys.validate_api_key(value="test_value") + finally: + workos.close() + + def test_validate_api_key_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.api_keys.validate_api_key(value="test_value") + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncApiKeys: + async def test_validate_api_key(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("api_key_validation_response.json")) + result = await async_workos.api_keys.validate_api_key(value="test_value") + assert isinstance(result, ApiKeyValidationResponse) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/api_keys/validations") + + async def test_delete_api_key(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.api_keys.delete_api_key("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/api_keys/test_id") + + async def test_validate_api_key_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.api_keys.validate_api_key(value="test_value") + + async def test_validate_api_key_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.api_keys.validate_api_key(value="test_value") + finally: + await workos.close() + + async def test_validate_api_key_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.api_keys.validate_api_key(value="test_value") + finally: + await workos.close() + + async def test_validate_api_key_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.api_keys.validate_api_key(value="test_value") + finally: + await workos.close() diff --git a/tests/test_application_client_secrets.py b/tests/test_application_client_secrets.py new file mode 100644 index 00000000..317c5167 --- /dev/null +++ b/tests/test_application_client_secrets.py @@ -0,0 +1,161 @@ +# This file is auto-generated by oagen. Do not edit. + + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.application_client_secrets.models import ( + ApplicationCredentialsListItem, + NewConnectApplicationSecret, +) +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestApplicationClientSecrets: + def test_list(self, workos, httpx_mock): + httpx_mock.add_response( + json=[load_fixture("application_credentials_list_item.json")] + ) + result = workos.application_client_secrets.list("test_id") + assert isinstance(result, list) + assert len(result) == 1 + assert isinstance(result[0], ApplicationCredentialsListItem) + + def test_create(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("new_connect_application_secret.json"), + ) + result = workos.application_client_secrets.create("test_id") + assert isinstance(result, NewConnectApplicationSecret) + assert result.object == "connect_application_secret" + assert result.id == "secret_01J9Q2Z3X4Y5W6V7U8T9S0R1Q" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/connect/applications/test_id/client_secrets") + + def test_delete(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.application_client_secrets.delete("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/connect/client_secrets/test_id") + + def test_list_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.application_client_secrets.list("test_id") + + def test_list_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.application_client_secrets.list("test_id") + finally: + workos.close() + + def test_list_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.application_client_secrets.list("test_id") + finally: + workos.close() + + def test_list_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.application_client_secrets.list("test_id") + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncApplicationClientSecrets: + async def test_list(self, async_workos, httpx_mock): + httpx_mock.add_response( + json=[load_fixture("application_credentials_list_item.json")] + ) + result = await async_workos.application_client_secrets.list("test_id") + assert isinstance(result, list) + assert len(result) == 1 + assert isinstance(result[0], ApplicationCredentialsListItem) + + async def test_create(self, async_workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("new_connect_application_secret.json") + ) + result = await async_workos.application_client_secrets.create("test_id") + assert isinstance(result, NewConnectApplicationSecret) + assert result.object == "connect_application_secret" + assert result.id == "secret_01J9Q2Z3X4Y5W6V7U8T9S0R1Q" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/connect/applications/test_id/client_secrets") + + async def test_delete(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.application_client_secrets.delete("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/connect/client_secrets/test_id") + + async def test_list_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.application_client_secrets.list("test_id") + + async def test_list_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.application_client_secrets.list("test_id") + finally: + await workos.close() + + async def test_list_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.application_client_secrets.list("test_id") + finally: + await workos.close() + + async def test_list_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.application_client_secrets.list("test_id") + finally: + await workos.close() diff --git a/tests/test_applications.py b/tests/test_applications.py new file mode 100644 index 00000000..32fccac1 --- /dev/null +++ b/tests/test_applications.py @@ -0,0 +1,245 @@ +# This file is auto-generated by oagen. Do not edit. + + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.applications.models import ConnectApplication, ApplicationsOrder +from workos._pagination import AsyncPage, SyncPage +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestApplications: + def test_list(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_connect_application.json"), + ) + page = workos.applications.list() + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.applications.list() + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.applications.list( + limit=10, + before="cursor before", + after="cursor/after", + order=ApplicationsOrder("normal"), + organization_id="value organization_id/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["organization_id"] == "value organization_id/test" + + def test_create(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("connect_application.json"), + ) + result = workos.applications.create( + body=load_fixture("create_oauth_application.json") + ) + assert isinstance(result, ConnectApplication) + assert result.object == "connect_application" + assert result.id == "conn_app_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/connect/applications") + + def test_get(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("connect_application.json"), + ) + result = workos.applications.get("test_id") + assert isinstance(result, ConnectApplication) + assert result.object == "connect_application" + assert result.id == "conn_app_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/connect/applications/test_id") + + def test_update(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("connect_application.json"), + ) + result = workos.applications.update("test_id") + assert isinstance(result, ConnectApplication) + assert result.object == "connect_application" + assert result.id == "conn_app_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/connect/applications/test_id") + + def test_delete(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.applications.delete("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/connect/applications/test_id") + + def test_list_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.applications.list() + + def test_list_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.applications.list() + finally: + workos.close() + + def test_list_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.applications.list() + finally: + workos.close() + + def test_list_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.applications.list() + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncApplications: + async def test_list(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_connect_application.json")) + page = await async_workos.applications.list() + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.applications.list() + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.applications.list( + limit=10, + before="cursor before", + after="cursor/after", + order=ApplicationsOrder("normal"), + organization_id="value organization_id/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["organization_id"] == "value organization_id/test" + + async def test_create(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("connect_application.json")) + result = await async_workos.applications.create( + body=load_fixture("create_oauth_application.json") + ) + assert isinstance(result, ConnectApplication) + assert result.object == "connect_application" + assert result.id == "conn_app_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/connect/applications") + + async def test_get(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("connect_application.json")) + result = await async_workos.applications.get("test_id") + assert isinstance(result, ConnectApplication) + assert result.object == "connect_application" + assert result.id == "conn_app_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/connect/applications/test_id") + + async def test_update(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("connect_application.json")) + result = await async_workos.applications.update("test_id") + assert isinstance(result, ConnectApplication) + assert result.object == "connect_application" + assert result.id == "conn_app_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/connect/applications/test_id") + + async def test_delete(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.applications.delete("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/connect/applications/test_id") + + async def test_list_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.applications.list() + + async def test_list_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.applications.list() + finally: + await workos.close() + + async def test_list_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.applications.list() + finally: + await workos.close() + + async def test_list_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.applications.list() + finally: + await workos.close() diff --git a/tests/test_audit_logs.py b/tests/test_audit_logs.py new file mode 100644 index 00000000..0f497d5b --- /dev/null +++ b/tests/test_audit_logs.py @@ -0,0 +1,331 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.audit_logs.models import ( + AuditLogEventCreateResponse, + AuditLogEvent, + AuditLogExportJson, + AuditLogSchemaJson, + AuditLogsOrder, +) +from workos._pagination import AsyncPage, SyncPage +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestAuditLogs: + def test_list(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_audit_log_action_json.json"), + ) + page = workos.audit_logs.list() + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.audit_logs.list() + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.audit_logs.list( + limit=10, + before="cursor before", + after="cursor/after", + order=AuditLogsOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + def test_list_schemas(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_audit_log_schema_json.json"), + ) + page = workos.audit_logs.list_schemas("test_actionName") + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_schemas_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.audit_logs.list_schemas("test_actionName") + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_schemas_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.audit_logs.list_schemas( + "test_actionName", + limit=10, + before="cursor before", + after="cursor/after", + order=AuditLogsOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + def test_create_schema(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("audit_log_schema_json.json"), + ) + result = workos.audit_logs.create_schema("test_actionName", targets=[]) + assert isinstance(result, AuditLogSchemaJson) + assert result.object == "audit_log_schema" + assert result.version == 1 + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/audit_logs/actions/test_actionName/schemas") + body = json.loads(request.content) + assert "targets" in body + + def test_create_event(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("audit_log_event_create_response.json"), + ) + result = workos.audit_logs.create_event( + organization_id="test_organization_id", + event=AuditLogEvent.from_dict(load_fixture("audit_log_event.json")), + ) + assert isinstance(result, AuditLogEventCreateResponse) + assert result.success is True + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/audit_logs/events") + body = json.loads(request.content) + assert body["organization_id"] == "test_organization_id" + assert "event" in body + + def test_exports(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("audit_log_export_json.json"), + ) + result = workos.audit_logs.exports( + organization_id="test_organization_id", + range_start="test_range_start", + range_end="test_range_end", + ) + assert isinstance(result, AuditLogExportJson) + assert result.object == "audit_log_export" + assert result.id == "audit_log_export_01GBZK5MP7TD1YCFQHFR22180V" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/audit_logs/exports") + body = json.loads(request.content) + assert body["organization_id"] == "test_organization_id" + assert body["range_start"] == "test_range_start" + assert body["range_end"] == "test_range_end" + + def test_export(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("audit_log_export_json.json"), + ) + result = workos.audit_logs.export("test_auditLogExportId") + assert isinstance(result, AuditLogExportJson) + assert result.object == "audit_log_export" + assert result.id == "audit_log_export_01GBZK5MP7TD1YCFQHFR22180V" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/audit_logs/exports/test_auditLogExportId") + + def test_list_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.audit_logs.list() + + def test_list_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.audit_logs.list() + finally: + workos.close() + + def test_list_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.audit_logs.list() + finally: + workos.close() + + def test_list_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.audit_logs.list() + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncAuditLogs: + async def test_list(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_audit_log_action_json.json")) + page = await async_workos.audit_logs.list() + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.audit_logs.list() + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.audit_logs.list( + limit=10, + before="cursor before", + after="cursor/after", + order=AuditLogsOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + async def test_list_schemas(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_audit_log_schema_json.json")) + page = await async_workos.audit_logs.list_schemas("test_actionName") + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_schemas_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.audit_logs.list_schemas("test_actionName") + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_schemas_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.audit_logs.list_schemas( + "test_actionName", + limit=10, + before="cursor before", + after="cursor/after", + order=AuditLogsOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + async def test_create_schema(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("audit_log_schema_json.json")) + result = await async_workos.audit_logs.create_schema( + "test_actionName", targets=[] + ) + assert isinstance(result, AuditLogSchemaJson) + assert result.object == "audit_log_schema" + assert result.version == 1 + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/audit_logs/actions/test_actionName/schemas") + + async def test_create_event(self, async_workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("audit_log_event_create_response.json") + ) + result = await async_workos.audit_logs.create_event( + organization_id="test_organization_id", + event=AuditLogEvent.from_dict(load_fixture("audit_log_event.json")), + ) + assert isinstance(result, AuditLogEventCreateResponse) + assert result.success is True + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/audit_logs/events") + + async def test_exports(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("audit_log_export_json.json")) + result = await async_workos.audit_logs.exports( + organization_id="test_organization_id", + range_start="test_range_start", + range_end="test_range_end", + ) + assert isinstance(result, AuditLogExportJson) + assert result.object == "audit_log_export" + assert result.id == "audit_log_export_01GBZK5MP7TD1YCFQHFR22180V" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/audit_logs/exports") + + async def test_export(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("audit_log_export_json.json")) + result = await async_workos.audit_logs.export("test_auditLogExportId") + assert isinstance(result, AuditLogExportJson) + assert result.object == "audit_log_export" + assert result.id == "audit_log_export_01GBZK5MP7TD1YCFQHFR22180V" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/audit_logs/exports/test_auditLogExportId") + + async def test_list_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.audit_logs.list() + + async def test_list_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.audit_logs.list() + finally: + await workos.close() + + async def test_list_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.audit_logs.list() + finally: + await workos.close() + + async def test_list_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.audit_logs.list() + finally: + await workos.close() diff --git a/tests/test_authorization.py b/tests/test_authorization.py new file mode 100644 index 00000000..0d3fce46 --- /dev/null +++ b/tests/test_authorization.py @@ -0,0 +1,1298 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.authorization.models import ( + AuthorizationCheck, + AuthorizationResource, + ListModel, + Role, + RoleAssignment, + RoleList, + AuthorizationAssignment, + AuthorizationOrder, +) +from workos._pagination import AsyncPage, SyncPage +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestAuthorization: + def test_check(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("authorization_check.json"), + ) + result = workos.authorization.check( + "test_organization_membership_id", permission_slug="test_permission_slug" + ) + assert isinstance(result, AuthorizationCheck) + assert result.authorized is True + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith( + "/authorization/organization_memberships/test_organization_membership_id/check" + ) + body = json.loads(request.content) + assert body["permission_slug"] == "test_permission_slug" + + def test_list_resources_for_membership(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_authorization_resource.json"), + ) + page = workos.authorization.list_resources_for_membership( + "test_organization_membership_id", permission_slug="test_permission_slug" + ) + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_resources_for_membership_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.authorization.list_resources_for_membership( + "test_organization_membership_id", permission_slug="test_permission_slug" + ) + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_resources_for_membership_encodes_query_params( + self, workos, httpx_mock + ): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.authorization.list_resources_for_membership( + "test_organization_membership_id", + limit=10, + before="cursor before", + after="cursor/after", + order=AuthorizationOrder("normal"), + permission_slug="value permission_slug/test", + parent_resource_id="value parent_resource_id/test", + parent_resource_type_slug="value parent_resource_type_slug/test", + parent_resource_external_id="value parent_resource_external_id/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["permission_slug"] == "value permission_slug/test" + assert ( + request.url.params["parent_resource_id"] == "value parent_resource_id/test" + ) + assert ( + request.url.params["parent_resource_type_slug"] + == "value parent_resource_type_slug/test" + ) + assert ( + request.url.params["parent_resource_external_id"] + == "value parent_resource_external_id/test" + ) + + def test_list_role_assignments(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_role_assignment.json"), + ) + page = workos.authorization.list_role_assignments( + "test_organization_membership_id" + ) + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_role_assignments_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.authorization.list_role_assignments( + "test_organization_membership_id" + ) + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_role_assignments_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.authorization.list_role_assignments( + "test_organization_membership_id", + limit=10, + before="cursor before", + after="cursor/after", + order=AuthorizationOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + def test_assign_role(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("role_assignment.json"), + ) + result = workos.authorization.assign_role( + "test_organization_membership_id", role_slug="test_role_slug" + ) + assert isinstance(result, RoleAssignment) + assert result.object == "role_assignment" + assert result.id == "role_assignment_01HXYZ123456789ABCDEFGH" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith( + "/authorization/organization_memberships/test_organization_membership_id/role_assignments" + ) + body = json.loads(request.content) + assert body["role_slug"] == "test_role_slug" + + def test_remove_role(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.authorization.remove_role( + "test_organization_membership_id", role_slug="test_role_slug" + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith( + "/authorization/organization_memberships/test_organization_membership_id/role_assignments" + ) + + def test_remove_role_by_id(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.authorization.remove_role_by_id( + "test_organization_membership_id", "test_role_assignment_id" + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith( + "/authorization/organization_memberships/test_organization_membership_id/role_assignments/test_role_assignment_id" + ) + + def test_list_roles_organizations(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list.json"), + ) + result = workos.authorization.list_roles_organizations("test_organizationId") + assert isinstance(result, ListModel) + assert result.object == "list" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith( + "/authorization/organizations/test_organizationId/roles" + ) + + def test_create_roles_organizations(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("role.json"), + ) + result = workos.authorization.create_roles_organizations( + "test_organizationId", slug="test_slug", name="test_name" + ) + assert isinstance(result, Role) + assert result.slug == "admin" + assert result.object == "role" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith( + "/authorization/organizations/test_organizationId/roles" + ) + body = json.loads(request.content) + assert body["slug"] == "test_slug" + assert body["name"] == "test_name" + + def test_get_roles_organization(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("role.json"), + ) + result = workos.authorization.get_roles_organization( + "test_organizationId", "test_slug" + ) + assert isinstance(result, Role) + assert result.slug == "admin" + assert result.object == "role" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith( + "/authorization/organizations/test_organizationId/roles/test_slug" + ) + + def test_update_roles_organizations(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("role.json"), + ) + result = workos.authorization.update_roles_organizations( + "test_organizationId", "test_slug" + ) + assert isinstance(result, Role) + assert result.slug == "admin" + assert result.object == "role" + request = httpx_mock.get_request() + assert request.method == "PATCH" + assert request.url.path.endswith( + "/authorization/organizations/test_organizationId/roles/test_slug" + ) + + def test_delete_roles(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.authorization.delete_roles("test_organizationId", "test_slug") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith( + "/authorization/organizations/test_organizationId/roles/test_slug" + ) + + def test_add_permission_permissions_organizations_roles(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("role.json"), + ) + result = workos.authorization.add_permission_permissions_organizations_roles( + "test_organizationId", "test_slug", body_slug="test_slug" + ) + assert isinstance(result, Role) + assert result.slug == "admin" + assert result.object == "role" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith( + "/authorization/organizations/test_organizationId/roles/test_slug/permissions" + ) + body = json.loads(request.content) + assert body["slug"] == "test_slug" + + def test_set_permissions_permissions_organizations_roles(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("role.json"), + ) + result = workos.authorization.set_permissions_permissions_organizations_roles( + "test_organizationId", "test_slug", permissions=[] + ) + assert isinstance(result, Role) + assert result.slug == "admin" + assert result.object == "role" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith( + "/authorization/organizations/test_organizationId/roles/test_slug/permissions" + ) + body = json.loads(request.content) + assert "permissions" in body + + def test_remove_permission(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.authorization.remove_permission( + "test_organizationId", "test_slug", "test_permissionSlug" + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith( + "/authorization/organizations/test_organizationId/roles/test_slug/permissions/test_permissionSlug" + ) + + def test_get_by_external_id(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("authorization_resource.json"), + ) + result = workos.authorization.get_by_external_id( + "test_organization_id", "test_resource_type_slug", "test_external_id" + ) + assert isinstance(result, AuthorizationResource) + assert result.object == "authorization_resource" + assert result.name == "Website Redesign" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith( + "/authorization/organizations/test_organization_id/resources/test_resource_type_slug/test_external_id" + ) + + def test_update_by_external_id(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("authorization_resource.json"), + ) + result = workos.authorization.update_by_external_id( + "test_organization_id", "test_resource_type_slug", "test_external_id" + ) + assert isinstance(result, AuthorizationResource) + assert result.object == "authorization_resource" + assert result.name == "Website Redesign" + request = httpx_mock.get_request() + assert request.method == "PATCH" + assert request.url.path.endswith( + "/authorization/organizations/test_organization_id/resources/test_resource_type_slug/test_external_id" + ) + + def test_delete_by_external_id(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.authorization.delete_by_external_id( + "test_organization_id", "test_resource_type_slug", "test_external_id" + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith( + "/authorization/organizations/test_organization_id/resources/test_resource_type_slug/test_external_id" + ) + + def test_delete_by_external_id_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + workos.authorization.delete_by_external_id( + "test_organization_id", + "test_resource_type_slug", + "test_external_id", + cascade_delete=True, + ) + request = httpx_mock.get_request() + assert request.url.params["cascade_delete"] == "true" + + def test_list_organization_memberships_for_resource_by_external_id( + self, workos, httpx_mock + ): + httpx_mock.add_response( + json=load_fixture("list_user_organization_membership_base_list_data.json"), + ) + page = workos.authorization.list_organization_memberships_for_resource_by_external_id( + "test_organization_id", + "test_resource_type_slug", + "test_external_id", + permission_slug="test_permission_slug", + ) + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_organization_memberships_for_resource_by_external_id_empty_page( + self, workos, httpx_mock + ): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.authorization.list_organization_memberships_for_resource_by_external_id( + "test_organization_id", + "test_resource_type_slug", + "test_external_id", + permission_slug="test_permission_slug", + ) + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_organization_memberships_for_resource_by_external_id_encodes_query_params( + self, workos, httpx_mock + ): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.authorization.list_organization_memberships_for_resource_by_external_id( + "test_organization_id", + "test_resource_type_slug", + "test_external_id", + limit=10, + before="cursor before", + after="cursor/after", + order=AuthorizationOrder("normal"), + permission_slug="value permission_slug/test", + assignment=AuthorizationAssignment("direct"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["permission_slug"] == "value permission_slug/test" + assert request.url.params["assignment"] == "direct" + + def test_list_resources(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_authorization_resource.json"), + ) + page = workos.authorization.list_resources() + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_resources_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.authorization.list_resources() + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_resources_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.authorization.list_resources( + limit=10, + before="cursor before", + after="cursor/after", + order=AuthorizationOrder("normal"), + organization_id="value organization_id/test", + resource_type_slug="value resource_type_slug/test", + parent_resource_id="value parent_resource_id/test", + parent_resource_type_slug="value parent_resource_type_slug/test", + parent_external_id="value parent_external_id/test", + search="value search/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["organization_id"] == "value organization_id/test" + assert ( + request.url.params["resource_type_slug"] == "value resource_type_slug/test" + ) + assert ( + request.url.params["parent_resource_id"] == "value parent_resource_id/test" + ) + assert ( + request.url.params["parent_resource_type_slug"] + == "value parent_resource_type_slug/test" + ) + assert ( + request.url.params["parent_external_id"] == "value parent_external_id/test" + ) + assert request.url.params["search"] == "value search/test" + + def test_create_resource(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("authorization_resource.json"), + ) + result = workos.authorization.create_resource( + external_id="test_external_id", + name="test_name", + resource_type_slug="test_resource_type_slug", + organization_id="test_organization_id", + ) + assert isinstance(result, AuthorizationResource) + assert result.object == "authorization_resource" + assert result.name == "Website Redesign" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/authorization/resources") + body = json.loads(request.content) + assert body["external_id"] == "test_external_id" + assert body["name"] == "test_name" + assert body["resource_type_slug"] == "test_resource_type_slug" + assert body["organization_id"] == "test_organization_id" + + def test_get_by_id(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("authorization_resource.json"), + ) + result = workos.authorization.get_by_id("test_resource_id") + assert isinstance(result, AuthorizationResource) + assert result.object == "authorization_resource" + assert result.name == "Website Redesign" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/authorization/resources/test_resource_id") + + def test_update_resource(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("authorization_resource.json"), + ) + result = workos.authorization.update_resource("test_resource_id") + assert isinstance(result, AuthorizationResource) + assert result.object == "authorization_resource" + assert result.name == "Website Redesign" + request = httpx_mock.get_request() + assert request.method == "PATCH" + assert request.url.path.endswith("/authorization/resources/test_resource_id") + + def test_delete_resource(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.authorization.delete_resource("test_resource_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/authorization/resources/test_resource_id") + + def test_delete_resource_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + workos.authorization.delete_resource("test_resource_id", cascade_delete=True) + request = httpx_mock.get_request() + assert request.url.params["cascade_delete"] == "true" + + def test_list_organization_memberships_for_resource(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_user_organization_membership_base_list_data.json"), + ) + page = workos.authorization.list_organization_memberships_for_resource( + "test_resource_id", permission_slug="test_permission_slug" + ) + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_organization_memberships_for_resource_empty_page( + self, workos, httpx_mock + ): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.authorization.list_organization_memberships_for_resource( + "test_resource_id", permission_slug="test_permission_slug" + ) + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_organization_memberships_for_resource_encodes_query_params( + self, workos, httpx_mock + ): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.authorization.list_organization_memberships_for_resource( + "test_resource_id", + limit=10, + before="cursor before", + after="cursor/after", + order=AuthorizationOrder("normal"), + permission_slug="value permission_slug/test", + assignment=AuthorizationAssignment("direct"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["permission_slug"] == "value permission_slug/test" + assert request.url.params["assignment"] == "direct" + + def test_list_roles(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("role_list.json"), + ) + result = workos.authorization.list_roles() + assert isinstance(result, RoleList) + assert result.object == "list" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/authorization/roles") + + def test_create_roles(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("role.json"), + ) + result = workos.authorization.create_roles(slug="test_slug", name="test_name") + assert isinstance(result, Role) + assert result.slug == "admin" + assert result.object == "role" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/authorization/roles") + body = json.loads(request.content) + assert body["slug"] == "test_slug" + assert body["name"] == "test_name" + + def test_get_roles(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("role.json"), + ) + result = workos.authorization.get_roles("test_slug") + assert isinstance(result, Role) + assert result.slug == "admin" + assert result.object == "role" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/authorization/roles/test_slug") + + def test_update_roles(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("role.json"), + ) + result = workos.authorization.update_roles("test_slug") + assert isinstance(result, Role) + assert result.slug == "admin" + assert result.object == "role" + request = httpx_mock.get_request() + assert request.method == "PATCH" + assert request.url.path.endswith("/authorization/roles/test_slug") + + def test_add_permission_permissions_roles(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("role.json"), + ) + result = workos.authorization.add_permission_permissions_roles( + "test_slug", body_slug="test_slug" + ) + assert isinstance(result, Role) + assert result.slug == "admin" + assert result.object == "role" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/authorization/roles/test_slug/permissions") + body = json.loads(request.content) + assert body["slug"] == "test_slug" + + def test_set_permissions_permissions_roles(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("role.json"), + ) + result = workos.authorization.set_permissions_permissions_roles( + "test_slug", permissions=[] + ) + assert isinstance(result, Role) + assert result.slug == "admin" + assert result.object == "role" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/authorization/roles/test_slug/permissions") + body = json.loads(request.content) + assert "permissions" in body + + def test_check_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.authorization.check( + "test_organization_membership_id", + permission_slug="test_permission_slug", + ) + + def test_check_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.authorization.check( + "test_organization_membership_id", + permission_slug="test_permission_slug", + ) + finally: + workos.close() + + def test_check_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.authorization.check( + "test_organization_membership_id", + permission_slug="test_permission_slug", + ) + finally: + workos.close() + + def test_check_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.authorization.check( + "test_organization_membership_id", + permission_slug="test_permission_slug", + ) + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncAuthorization: + async def test_check(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("authorization_check.json")) + result = await async_workos.authorization.check( + "test_organization_membership_id", permission_slug="test_permission_slug" + ) + assert isinstance(result, AuthorizationCheck) + assert result.authorized is True + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith( + "/authorization/organization_memberships/test_organization_membership_id/check" + ) + + async def test_list_resources_for_membership(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_authorization_resource.json")) + page = await async_workos.authorization.list_resources_for_membership( + "test_organization_membership_id", permission_slug="test_permission_slug" + ) + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_resources_for_membership_empty_page( + self, async_workos, httpx_mock + ): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.authorization.list_resources_for_membership( + "test_organization_membership_id", permission_slug="test_permission_slug" + ) + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_resources_for_membership_encodes_query_params( + self, async_workos, httpx_mock + ): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.authorization.list_resources_for_membership( + "test_organization_membership_id", + limit=10, + before="cursor before", + after="cursor/after", + order=AuthorizationOrder("normal"), + permission_slug="value permission_slug/test", + parent_resource_id="value parent_resource_id/test", + parent_resource_type_slug="value parent_resource_type_slug/test", + parent_resource_external_id="value parent_resource_external_id/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["permission_slug"] == "value permission_slug/test" + assert ( + request.url.params["parent_resource_id"] == "value parent_resource_id/test" + ) + assert ( + request.url.params["parent_resource_type_slug"] + == "value parent_resource_type_slug/test" + ) + assert ( + request.url.params["parent_resource_external_id"] + == "value parent_resource_external_id/test" + ) + + async def test_list_role_assignments(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_role_assignment.json")) + page = await async_workos.authorization.list_role_assignments( + "test_organization_membership_id" + ) + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_role_assignments_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.authorization.list_role_assignments( + "test_organization_membership_id" + ) + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_role_assignments_encodes_query_params( + self, async_workos, httpx_mock + ): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.authorization.list_role_assignments( + "test_organization_membership_id", + limit=10, + before="cursor before", + after="cursor/after", + order=AuthorizationOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + async def test_assign_role(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("role_assignment.json")) + result = await async_workos.authorization.assign_role( + "test_organization_membership_id", role_slug="test_role_slug" + ) + assert isinstance(result, RoleAssignment) + assert result.object == "role_assignment" + assert result.id == "role_assignment_01HXYZ123456789ABCDEFGH" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith( + "/authorization/organization_memberships/test_organization_membership_id/role_assignments" + ) + + async def test_remove_role(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.authorization.remove_role( + "test_organization_membership_id", role_slug="test_role_slug" + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith( + "/authorization/organization_memberships/test_organization_membership_id/role_assignments" + ) + + async def test_remove_role_by_id(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.authorization.remove_role_by_id( + "test_organization_membership_id", "test_role_assignment_id" + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith( + "/authorization/organization_memberships/test_organization_membership_id/role_assignments/test_role_assignment_id" + ) + + async def test_list_roles_organizations(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list.json")) + result = await async_workos.authorization.list_roles_organizations( + "test_organizationId" + ) + assert isinstance(result, ListModel) + assert result.object == "list" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith( + "/authorization/organizations/test_organizationId/roles" + ) + + async def test_create_roles_organizations(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("role.json")) + result = await async_workos.authorization.create_roles_organizations( + "test_organizationId", slug="test_slug", name="test_name" + ) + assert isinstance(result, Role) + assert result.slug == "admin" + assert result.object == "role" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith( + "/authorization/organizations/test_organizationId/roles" + ) + + async def test_get_roles_organization(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("role.json")) + result = await async_workos.authorization.get_roles_organization( + "test_organizationId", "test_slug" + ) + assert isinstance(result, Role) + assert result.slug == "admin" + assert result.object == "role" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith( + "/authorization/organizations/test_organizationId/roles/test_slug" + ) + + async def test_update_roles_organizations(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("role.json")) + result = await async_workos.authorization.update_roles_organizations( + "test_organizationId", "test_slug" + ) + assert isinstance(result, Role) + assert result.slug == "admin" + assert result.object == "role" + request = httpx_mock.get_request() + assert request.method == "PATCH" + assert request.url.path.endswith( + "/authorization/organizations/test_organizationId/roles/test_slug" + ) + + async def test_delete_roles(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.authorization.delete_roles( + "test_organizationId", "test_slug" + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith( + "/authorization/organizations/test_organizationId/roles/test_slug" + ) + + async def test_add_permission_permissions_organizations_roles( + self, async_workos, httpx_mock + ): + httpx_mock.add_response(json=load_fixture("role.json")) + result = await async_workos.authorization.add_permission_permissions_organizations_roles( + "test_organizationId", "test_slug", body_slug="test_slug" + ) + assert isinstance(result, Role) + assert result.slug == "admin" + assert result.object == "role" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith( + "/authorization/organizations/test_organizationId/roles/test_slug/permissions" + ) + + async def test_set_permissions_permissions_organizations_roles( + self, async_workos, httpx_mock + ): + httpx_mock.add_response(json=load_fixture("role.json")) + result = await async_workos.authorization.set_permissions_permissions_organizations_roles( + "test_organizationId", "test_slug", permissions=[] + ) + assert isinstance(result, Role) + assert result.slug == "admin" + assert result.object == "role" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith( + "/authorization/organizations/test_organizationId/roles/test_slug/permissions" + ) + + async def test_remove_permission(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.authorization.remove_permission( + "test_organizationId", "test_slug", "test_permissionSlug" + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith( + "/authorization/organizations/test_organizationId/roles/test_slug/permissions/test_permissionSlug" + ) + + async def test_get_by_external_id(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("authorization_resource.json")) + result = await async_workos.authorization.get_by_external_id( + "test_organization_id", "test_resource_type_slug", "test_external_id" + ) + assert isinstance(result, AuthorizationResource) + assert result.object == "authorization_resource" + assert result.name == "Website Redesign" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith( + "/authorization/organizations/test_organization_id/resources/test_resource_type_slug/test_external_id" + ) + + async def test_update_by_external_id(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("authorization_resource.json")) + result = await async_workos.authorization.update_by_external_id( + "test_organization_id", "test_resource_type_slug", "test_external_id" + ) + assert isinstance(result, AuthorizationResource) + assert result.object == "authorization_resource" + assert result.name == "Website Redesign" + request = httpx_mock.get_request() + assert request.method == "PATCH" + assert request.url.path.endswith( + "/authorization/organizations/test_organization_id/resources/test_resource_type_slug/test_external_id" + ) + + async def test_delete_by_external_id(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.authorization.delete_by_external_id( + "test_organization_id", "test_resource_type_slug", "test_external_id" + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith( + "/authorization/organizations/test_organization_id/resources/test_resource_type_slug/test_external_id" + ) + + async def test_delete_by_external_id_encodes_query_params( + self, async_workos, httpx_mock + ): + httpx_mock.add_response(status_code=204) + await async_workos.authorization.delete_by_external_id( + "test_organization_id", + "test_resource_type_slug", + "test_external_id", + cascade_delete=True, + ) + request = httpx_mock.get_request() + assert request.url.params["cascade_delete"] == "true" + + async def test_list_organization_memberships_for_resource_by_external_id( + self, async_workos, httpx_mock + ): + httpx_mock.add_response( + json=load_fixture("list_user_organization_membership_base_list_data.json") + ) + page = await async_workos.authorization.list_organization_memberships_for_resource_by_external_id( + "test_organization_id", + "test_resource_type_slug", + "test_external_id", + permission_slug="test_permission_slug", + ) + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_organization_memberships_for_resource_by_external_id_empty_page( + self, async_workos, httpx_mock + ): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.authorization.list_organization_memberships_for_resource_by_external_id( + "test_organization_id", + "test_resource_type_slug", + "test_external_id", + permission_slug="test_permission_slug", + ) + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_organization_memberships_for_resource_by_external_id_encodes_query_params( + self, async_workos, httpx_mock + ): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.authorization.list_organization_memberships_for_resource_by_external_id( + "test_organization_id", + "test_resource_type_slug", + "test_external_id", + limit=10, + before="cursor before", + after="cursor/after", + order=AuthorizationOrder("normal"), + permission_slug="value permission_slug/test", + assignment=AuthorizationAssignment("direct"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["permission_slug"] == "value permission_slug/test" + assert request.url.params["assignment"] == "direct" + + async def test_list_resources(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_authorization_resource.json")) + page = await async_workos.authorization.list_resources() + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_resources_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.authorization.list_resources() + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_resources_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.authorization.list_resources( + limit=10, + before="cursor before", + after="cursor/after", + order=AuthorizationOrder("normal"), + organization_id="value organization_id/test", + resource_type_slug="value resource_type_slug/test", + parent_resource_id="value parent_resource_id/test", + parent_resource_type_slug="value parent_resource_type_slug/test", + parent_external_id="value parent_external_id/test", + search="value search/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["organization_id"] == "value organization_id/test" + assert ( + request.url.params["resource_type_slug"] == "value resource_type_slug/test" + ) + assert ( + request.url.params["parent_resource_id"] == "value parent_resource_id/test" + ) + assert ( + request.url.params["parent_resource_type_slug"] + == "value parent_resource_type_slug/test" + ) + assert ( + request.url.params["parent_external_id"] == "value parent_external_id/test" + ) + assert request.url.params["search"] == "value search/test" + + async def test_create_resource(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("authorization_resource.json")) + result = await async_workos.authorization.create_resource( + external_id="test_external_id", + name="test_name", + resource_type_slug="test_resource_type_slug", + organization_id="test_organization_id", + ) + assert isinstance(result, AuthorizationResource) + assert result.object == "authorization_resource" + assert result.name == "Website Redesign" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/authorization/resources") + + async def test_get_by_id(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("authorization_resource.json")) + result = await async_workos.authorization.get_by_id("test_resource_id") + assert isinstance(result, AuthorizationResource) + assert result.object == "authorization_resource" + assert result.name == "Website Redesign" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/authorization/resources/test_resource_id") + + async def test_update_resource(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("authorization_resource.json")) + result = await async_workos.authorization.update_resource("test_resource_id") + assert isinstance(result, AuthorizationResource) + assert result.object == "authorization_resource" + assert result.name == "Website Redesign" + request = httpx_mock.get_request() + assert request.method == "PATCH" + assert request.url.path.endswith("/authorization/resources/test_resource_id") + + async def test_delete_resource(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.authorization.delete_resource("test_resource_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/authorization/resources/test_resource_id") + + async def test_delete_resource_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + await async_workos.authorization.delete_resource( + "test_resource_id", cascade_delete=True + ) + request = httpx_mock.get_request() + assert request.url.params["cascade_delete"] == "true" + + async def test_list_organization_memberships_for_resource( + self, async_workos, httpx_mock + ): + httpx_mock.add_response( + json=load_fixture("list_user_organization_membership_base_list_data.json") + ) + page = ( + await async_workos.authorization.list_organization_memberships_for_resource( + "test_resource_id", permission_slug="test_permission_slug" + ) + ) + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_organization_memberships_for_resource_empty_page( + self, async_workos, httpx_mock + ): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = ( + await async_workos.authorization.list_organization_memberships_for_resource( + "test_resource_id", permission_slug="test_permission_slug" + ) + ) + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_organization_memberships_for_resource_encodes_query_params( + self, async_workos, httpx_mock + ): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.authorization.list_organization_memberships_for_resource( + "test_resource_id", + limit=10, + before="cursor before", + after="cursor/after", + order=AuthorizationOrder("normal"), + permission_slug="value permission_slug/test", + assignment=AuthorizationAssignment("direct"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["permission_slug"] == "value permission_slug/test" + assert request.url.params["assignment"] == "direct" + + async def test_list_roles(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("role_list.json")) + result = await async_workos.authorization.list_roles() + assert isinstance(result, RoleList) + assert result.object == "list" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/authorization/roles") + + async def test_create_roles(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("role.json")) + result = await async_workos.authorization.create_roles( + slug="test_slug", name="test_name" + ) + assert isinstance(result, Role) + assert result.slug == "admin" + assert result.object == "role" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/authorization/roles") + + async def test_get_roles(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("role.json")) + result = await async_workos.authorization.get_roles("test_slug") + assert isinstance(result, Role) + assert result.slug == "admin" + assert result.object == "role" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/authorization/roles/test_slug") + + async def test_update_roles(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("role.json")) + result = await async_workos.authorization.update_roles("test_slug") + assert isinstance(result, Role) + assert result.slug == "admin" + assert result.object == "role" + request = httpx_mock.get_request() + assert request.method == "PATCH" + assert request.url.path.endswith("/authorization/roles/test_slug") + + async def test_add_permission_permissions_roles(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("role.json")) + result = await async_workos.authorization.add_permission_permissions_roles( + "test_slug", body_slug="test_slug" + ) + assert isinstance(result, Role) + assert result.slug == "admin" + assert result.object == "role" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/authorization/roles/test_slug/permissions") + + async def test_set_permissions_permissions_roles(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("role.json")) + result = await async_workos.authorization.set_permissions_permissions_roles( + "test_slug", permissions=[] + ) + assert isinstance(result, Role) + assert result.slug == "admin" + assert result.object == "role" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/authorization/roles/test_slug/permissions") + + async def test_check_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.authorization.check( + "test_organization_membership_id", + permission_slug="test_permission_slug", + ) + + async def test_check_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.authorization.check( + "test_organization_membership_id", + permission_slug="test_permission_slug", + ) + finally: + await workos.close() + + async def test_check_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.authorization.check( + "test_organization_membership_id", + permission_slug="test_permission_slug", + ) + finally: + await workos.close() + + async def test_check_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.authorization.check( + "test_organization_membership_id", + permission_slug="test_permission_slug", + ) + finally: + await workos.close() diff --git a/tests/test_connections.py b/tests/test_connections.py new file mode 100644 index 00000000..9e28f33d --- /dev/null +++ b/tests/test_connections.py @@ -0,0 +1,213 @@ +# This file is auto-generated by oagen. Do not edit. + + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.connections.models import ( + Connection, + ConnectionsConnectionType, + ConnectionsOrder, +) +from workos._pagination import AsyncPage, SyncPage +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestConnections: + def test_list(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_connection.json"), + ) + page = workos.connections.list() + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.connections.list() + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.connections.list( + limit=10, + before="cursor before", + after="cursor/after", + order=ConnectionsOrder("normal"), + connection_type=ConnectionsConnectionType("ADFSSAML"), + domain="value domain/test", + organization_id="value organization_id/test", + search="value search/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["connection_type"] == "ADFSSAML" + assert request.url.params["domain"] == "value domain/test" + assert request.url.params["organization_id"] == "value organization_id/test" + assert request.url.params["search"] == "value search/test" + + def test_get(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("connection.json"), + ) + result = workos.connections.get("test_id") + assert isinstance(result, Connection) + assert result.object == "connection" + assert result.id == "conn_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/connections/test_id") + + def test_delete(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.connections.delete("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/connections/test_id") + + def test_list_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.connections.list() + + def test_list_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.connections.list() + finally: + workos.close() + + def test_list_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.connections.list() + finally: + workos.close() + + def test_list_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.connections.list() + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncConnections: + async def test_list(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_connection.json")) + page = await async_workos.connections.list() + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.connections.list() + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.connections.list( + limit=10, + before="cursor before", + after="cursor/after", + order=ConnectionsOrder("normal"), + connection_type=ConnectionsConnectionType("ADFSSAML"), + domain="value domain/test", + organization_id="value organization_id/test", + search="value search/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["connection_type"] == "ADFSSAML" + assert request.url.params["domain"] == "value domain/test" + assert request.url.params["organization_id"] == "value organization_id/test" + assert request.url.params["search"] == "value search/test" + + async def test_get(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("connection.json")) + result = await async_workos.connections.get("test_id") + assert isinstance(result, Connection) + assert result.object == "connection" + assert result.id == "conn_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/connections/test_id") + + async def test_delete(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.connections.delete("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/connections/test_id") + + async def test_list_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.connections.list() + + async def test_list_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.connections.list() + finally: + await workos.close() + + async def test_list_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.connections.list() + finally: + await workos.close() + + async def test_list_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.connections.list() + finally: + await workos.close() diff --git a/tests/test_directories.py b/tests/test_directories.py new file mode 100644 index 00000000..6d216452 --- /dev/null +++ b/tests/test_directories.py @@ -0,0 +1,205 @@ +# This file is auto-generated by oagen. Do not edit. + + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.directories.models import Directory, DirectoriesOrder +from workos._pagination import AsyncPage, SyncPage +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestDirectories: + def test_list(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_directory.json"), + ) + page = workos.directories.list() + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.directories.list() + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.directories.list( + limit=10, + before="cursor before", + after="cursor/after", + order=DirectoriesOrder("normal"), + organization_id="value organization_id/test", + search="value search/test", + domain="value domain/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["organization_id"] == "value organization_id/test" + assert request.url.params["search"] == "value search/test" + assert request.url.params["domain"] == "value domain/test" + + def test_get(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("directory.json"), + ) + result = workos.directories.get("test_id") + assert isinstance(result, Directory) + assert result.object == "directory" + assert result.id == "directory_01ECAZ4NV9QMV47GW873HDCX74" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/directories/test_id") + + def test_delete(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.directories.delete("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/directories/test_id") + + def test_list_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.directories.list() + + def test_list_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.directories.list() + finally: + workos.close() + + def test_list_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.directories.list() + finally: + workos.close() + + def test_list_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.directories.list() + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncDirectories: + async def test_list(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_directory.json")) + page = await async_workos.directories.list() + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.directories.list() + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.directories.list( + limit=10, + before="cursor before", + after="cursor/after", + order=DirectoriesOrder("normal"), + organization_id="value organization_id/test", + search="value search/test", + domain="value domain/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["organization_id"] == "value organization_id/test" + assert request.url.params["search"] == "value search/test" + assert request.url.params["domain"] == "value domain/test" + + async def test_get(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("directory.json")) + result = await async_workos.directories.get("test_id") + assert isinstance(result, Directory) + assert result.object == "directory" + assert result.id == "directory_01ECAZ4NV9QMV47GW873HDCX74" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/directories/test_id") + + async def test_delete(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.directories.delete("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/directories/test_id") + + async def test_list_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.directories.list() + + async def test_list_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.directories.list() + finally: + await workos.close() + + async def test_list_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.directories.list() + finally: + await workos.close() + + async def test_list_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.directories.list() + finally: + await workos.close() diff --git a/tests/test_directory_groups.py b/tests/test_directory_groups.py new file mode 100644 index 00000000..3ea6f30d --- /dev/null +++ b/tests/test_directory_groups.py @@ -0,0 +1,185 @@ +# This file is auto-generated by oagen. Do not edit. + + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.directory_groups.models import DirectoryGroup, DirectoryGroupsOrder +from workos._pagination import AsyncPage, SyncPage +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestDirectoryGroups: + def test_list(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_directory_group.json"), + ) + page = workos.directory_groups.list() + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.directory_groups.list() + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.directory_groups.list( + limit=10, + before="cursor before", + after="cursor/after", + order=DirectoryGroupsOrder("normal"), + directory="value directory/test", + user="value user/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["directory"] == "value directory/test" + assert request.url.params["user"] == "value user/test" + + def test_get(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("directory_group.json"), + ) + result = workos.directory_groups.get("test_id") + assert isinstance(result, DirectoryGroup) + assert result.object == "directory_group" + assert result.id == "directory_group_01E1JJS84MFPPQ3G655FHTKX6Z" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/directory_groups/test_id") + + def test_list_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.directory_groups.list() + + def test_list_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.directory_groups.list() + finally: + workos.close() + + def test_list_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.directory_groups.list() + finally: + workos.close() + + def test_list_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.directory_groups.list() + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncDirectoryGroups: + async def test_list(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_directory_group.json")) + page = await async_workos.directory_groups.list() + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.directory_groups.list() + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.directory_groups.list( + limit=10, + before="cursor before", + after="cursor/after", + order=DirectoryGroupsOrder("normal"), + directory="value directory/test", + user="value user/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["directory"] == "value directory/test" + assert request.url.params["user"] == "value user/test" + + async def test_get(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("directory_group.json")) + result = await async_workos.directory_groups.get("test_id") + assert isinstance(result, DirectoryGroup) + assert result.object == "directory_group" + assert result.id == "directory_group_01E1JJS84MFPPQ3G655FHTKX6Z" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/directory_groups/test_id") + + async def test_list_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.directory_groups.list() + + async def test_list_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.directory_groups.list() + finally: + await workos.close() + + async def test_list_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.directory_groups.list() + finally: + await workos.close() + + async def test_list_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.directory_groups.list() + finally: + await workos.close() diff --git a/tests/test_directory_users.py b/tests/test_directory_users.py new file mode 100644 index 00000000..8696eb9c --- /dev/null +++ b/tests/test_directory_users.py @@ -0,0 +1,187 @@ +# This file is auto-generated by oagen. Do not edit. + + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.directory_users.models import DirectoryUserWithGroups, DirectoryUsersOrder +from workos._pagination import AsyncPage, SyncPage +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestDirectoryUsers: + def test_list(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_directory_user_with_groups.json"), + ) + page = workos.directory_users.list() + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.directory_users.list() + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.directory_users.list( + limit=10, + before="cursor before", + after="cursor/after", + order=DirectoryUsersOrder("normal"), + directory="value directory/test", + group="value group/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["directory"] == "value directory/test" + assert request.url.params["group"] == "value group/test" + + def test_get(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("directory_user_with_groups.json"), + ) + result = workos.directory_users.get("test_id") + assert isinstance(result, DirectoryUserWithGroups) + assert result.object == "directory_user" + assert result.id == "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/directory_users/test_id") + + def test_list_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.directory_users.list() + + def test_list_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.directory_users.list() + finally: + workos.close() + + def test_list_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.directory_users.list() + finally: + workos.close() + + def test_list_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.directory_users.list() + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncDirectoryUsers: + async def test_list(self, async_workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_directory_user_with_groups.json") + ) + page = await async_workos.directory_users.list() + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.directory_users.list() + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.directory_users.list( + limit=10, + before="cursor before", + after="cursor/after", + order=DirectoryUsersOrder("normal"), + directory="value directory/test", + group="value group/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["directory"] == "value directory/test" + assert request.url.params["group"] == "value group/test" + + async def test_get(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("directory_user_with_groups.json")) + result = await async_workos.directory_users.get("test_id") + assert isinstance(result, DirectoryUserWithGroups) + assert result.object == "directory_user" + assert result.id == "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/directory_users/test_id") + + async def test_list_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.directory_users.list() + + async def test_list_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.directory_users.list() + finally: + await workos.close() + + async def test_list_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.directory_users.list() + finally: + await workos.close() + + async def test_list_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.directory_users.list() + finally: + await workos.close() diff --git a/tests/test_events.py b/tests/test_events.py new file mode 100644 index 00000000..0e9239eb --- /dev/null +++ b/tests/test_events.py @@ -0,0 +1,167 @@ +# This file is auto-generated by oagen. Do not edit. + + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.events.models import EventsOrder +from workos._pagination import AsyncPage, SyncPage +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestEvents: + def test_list_events(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_event.json"), + ) + page = workos.events.list_events() + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_events_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.events.list_events() + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_events_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.events.list_events( + limit=10, + before="cursor before", + after="cursor/after", + order=EventsOrder("normal"), + range_start="value range_start/test", + range_end="value range_end/test", + organization_id="value organization_id/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["range_start"] == "value range_start/test" + assert request.url.params["range_end"] == "value range_end/test" + assert request.url.params["organization_id"] == "value organization_id/test" + + def test_list_events_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.events.list_events() + + def test_list_events_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.events.list_events() + finally: + workos.close() + + def test_list_events_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.events.list_events() + finally: + workos.close() + + def test_list_events_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.events.list_events() + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncEvents: + async def test_list_events(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_event.json")) + page = await async_workos.events.list_events() + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_events_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.events.list_events() + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_events_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.events.list_events( + limit=10, + before="cursor before", + after="cursor/after", + order=EventsOrder("normal"), + range_start="value range_start/test", + range_end="value range_end/test", + organization_id="value organization_id/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["range_start"] == "value range_start/test" + assert request.url.params["range_end"] == "value range_end/test" + assert request.url.params["organization_id"] == "value organization_id/test" + + async def test_list_events_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.events.list_events() + + async def test_list_events_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.events.list_events() + finally: + await workos.close() + + async def test_list_events_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.events.list_events() + finally: + await workos.close() + + async def test_list_events_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.events.list_events() + finally: + await workos.close() diff --git a/tests/test_feature_flags.py b/tests/test_feature_flags.py new file mode 100644 index 00000000..b85b3bbb --- /dev/null +++ b/tests/test_feature_flags.py @@ -0,0 +1,221 @@ +# This file is auto-generated by oagen. Do not edit. + + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.feature_flags.models import FeatureFlag, Flag, FeatureFlagsOrder +from workos._pagination import AsyncPage, SyncPage +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestFeatureFlags: + def test_list(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_flag.json"), + ) + page = workos.feature_flags.list() + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.feature_flags.list() + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.feature_flags.list( + limit=10, + before="cursor before", + after="cursor/after", + order=FeatureFlagsOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + def test_get_by_slug(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("flag.json"), + ) + result = workos.feature_flags.get_by_slug("test_slug") + assert isinstance(result, Flag) + assert result.object == "feature_flag" + assert result.id == "flag_01EHZNVPK3SFK441A1RGBFSHRT" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/feature-flags/test_slug") + + def test_disable_flag(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("feature_flag.json"), + ) + result = workos.feature_flags.disable_flag("test_slug") + assert isinstance(result, FeatureFlag) + assert result.object == "feature_flag" + assert result.id == "flag_01EHZNVPK3SFK441A1RGBFSHRT" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/feature-flags/test_slug/disable") + + def test_enable_flag(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("feature_flag.json"), + ) + result = workos.feature_flags.enable_flag("test_slug") + assert isinstance(result, FeatureFlag) + assert result.object == "feature_flag" + assert result.id == "flag_01EHZNVPK3SFK441A1RGBFSHRT" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/feature-flags/test_slug/enable") + + def test_list_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.feature_flags.list() + + def test_list_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.feature_flags.list() + finally: + workos.close() + + def test_list_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.feature_flags.list() + finally: + workos.close() + + def test_list_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.feature_flags.list() + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncFeatureFlags: + async def test_list(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_flag.json")) + page = await async_workos.feature_flags.list() + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.feature_flags.list() + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.feature_flags.list( + limit=10, + before="cursor before", + after="cursor/after", + order=FeatureFlagsOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + async def test_get_by_slug(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("flag.json")) + result = await async_workos.feature_flags.get_by_slug("test_slug") + assert isinstance(result, Flag) + assert result.object == "feature_flag" + assert result.id == "flag_01EHZNVPK3SFK441A1RGBFSHRT" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/feature-flags/test_slug") + + async def test_disable_flag(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("feature_flag.json")) + result = await async_workos.feature_flags.disable_flag("test_slug") + assert isinstance(result, FeatureFlag) + assert result.object == "feature_flag" + assert result.id == "flag_01EHZNVPK3SFK441A1RGBFSHRT" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/feature-flags/test_slug/disable") + + async def test_enable_flag(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("feature_flag.json")) + result = await async_workos.feature_flags.enable_flag("test_slug") + assert isinstance(result, FeatureFlag) + assert result.object == "feature_flag" + assert result.id == "flag_01EHZNVPK3SFK441A1RGBFSHRT" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/feature-flags/test_slug/enable") + + async def test_list_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.feature_flags.list() + + async def test_list_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.feature_flags.list() + finally: + await workos.close() + + async def test_list_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.feature_flags.list() + finally: + await workos.close() + + async def test_list_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.feature_flags.list() + finally: + await workos.close() diff --git a/tests/test_feature_flags_targets.py b/tests/test_feature_flags_targets.py new file mode 100644 index 00000000..deccd406 --- /dev/null +++ b/tests/test_feature_flags_targets.py @@ -0,0 +1,156 @@ +# This file is auto-generated by oagen. Do not edit. + + +import pytest +from workos import WorkOS, AsyncWorkOS + +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestFeatureFlagsTargets: + def test_create_target(self, workos, httpx_mock): + httpx_mock.add_response(json={}) + workos.feature_flags.targets.create_target("test_resourceId", "test_slug") + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith( + "/feature-flags/test_slug/targets/test_resourceId" + ) + + def test_delete_target(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.feature_flags.targets.delete_target( + "test_resourceId", "test_slug" + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith( + "/feature-flags/test_slug/targets/test_resourceId" + ) + + def test_create_target_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.feature_flags.targets.create_target("test_resourceId", "test_slug") + + def test_create_target_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.feature_flags.targets.create_target( + "test_resourceId", "test_slug" + ) + finally: + workos.close() + + def test_create_target_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.feature_flags.targets.create_target( + "test_resourceId", "test_slug" + ) + finally: + workos.close() + + def test_create_target_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.feature_flags.targets.create_target( + "test_resourceId", "test_slug" + ) + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncFeatureFlagsTargets: + async def test_create_target(self, async_workos, httpx_mock): + httpx_mock.add_response(json={}) + await async_workos.feature_flags.targets.create_target( + "test_resourceId", "test_slug" + ) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith( + "/feature-flags/test_slug/targets/test_resourceId" + ) + + async def test_delete_target(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.feature_flags.targets.delete_target( + "test_resourceId", "test_slug" + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith( + "/feature-flags/test_slug/targets/test_resourceId" + ) + + async def test_create_target_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.feature_flags.targets.create_target( + "test_resourceId", "test_slug" + ) + + async def test_create_target_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.feature_flags.targets.create_target( + "test_resourceId", "test_slug" + ) + finally: + await workos.close() + + async def test_create_target_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.feature_flags.targets.create_target( + "test_resourceId", "test_slug" + ) + finally: + await workos.close() + + async def test_create_target_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.feature_flags.targets.create_target( + "test_resourceId", "test_slug" + ) + finally: + await workos.close() diff --git a/tests/test_generated_client.py b/tests/test_generated_client.py new file mode 100644 index 00000000..4a574300 --- /dev/null +++ b/tests/test_generated_client.py @@ -0,0 +1,297 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Client tests: retries, errors, context manager, idempotency.""" + +import httpx +import pytest + +from workos import WorkOS, AsyncWorkOS +from workos import _client as generated_client_module +from workos._errors import ( + AuthenticationException, + BadRequestException, + AuthorizationException, + NotFoundException, + ConflictException, + UnprocessableEntityException, + RateLimitExceededException, + ServerException, +) + + +class TestWorkOSClient: + def test_missing_api_key_raises(self): + with pytest.raises(ValueError): + WorkOS(client_id="client_test") + + def test_context_manager(self): + with WorkOS(api_key="sk_test_123", client_id="client_test") as client: + assert client._api_key == "sk_test_123" + + def test_client_id_from_constructor(self): + client = WorkOS(api_key="sk_test_123", client_id="client_test_456") + assert client.client_id == "client_test_456" + client.close() + + def test_raises_400(self, httpx_mock): + httpx_mock.add_response( + status_code=400, + json={"message": "Error"}, + ) + client = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + with pytest.raises(BadRequestException): + client.request("GET", "test") + client.close() + + def test_raises_401(self, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Error"}, + ) + client = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + with pytest.raises(AuthenticationException): + client.request("GET", "test") + client.close() + + def test_raises_403(self, httpx_mock): + httpx_mock.add_response( + status_code=403, + json={"message": "Error"}, + ) + client = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + with pytest.raises(AuthorizationException): + client.request("GET", "test") + client.close() + + def test_raises_404(self, httpx_mock): + httpx_mock.add_response( + status_code=404, + json={"message": "Error"}, + ) + client = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + with pytest.raises(NotFoundException): + client.request("GET", "test") + client.close() + + def test_raises_409(self, httpx_mock): + httpx_mock.add_response( + status_code=409, + json={"message": "Error"}, + ) + client = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + with pytest.raises(ConflictException): + client.request("GET", "test") + client.close() + + def test_raises_422(self, httpx_mock): + httpx_mock.add_response( + status_code=422, + json={"message": "Error"}, + ) + client = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + with pytest.raises(UnprocessableEntityException): + client.request("GET", "test") + client.close() + + def test_raises_429(self, httpx_mock): + httpx_mock.add_response( + status_code=429, + json={"message": "Error"}, + ) + client = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + with pytest.raises(RateLimitExceededException): + client.request("GET", "test") + client.close() + + def test_raises_500(self, httpx_mock): + httpx_mock.add_response( + status_code=500, + json={"message": "Error"}, + ) + client = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + with pytest.raises(ServerException): + client.request("GET", "test") + client.close() + + def test_idempotency_key_on_post(self, httpx_mock): + httpx_mock.add_response(json={}) + client = WorkOS(api_key="sk_test_123", client_id="client_test") + client.request("POST", "test") + request = httpx_mock.get_request() + assert "Idempotency-Key" in request.headers + client.close() + + def test_no_idempotency_key_on_get(self, httpx_mock): + httpx_mock.add_response(json={}) + client = WorkOS(api_key="sk_test_123", client_id="client_test") + client.request("GET", "test") + request = httpx_mock.get_request() + assert "Idempotency-Key" not in request.headers + client.close() + + def test_empty_body_sends_json(self, httpx_mock): + httpx_mock.add_response(json={}) + client = WorkOS(api_key="sk_test_123", client_id="client_test") + client.request("PUT", "test", body={}) + request = httpx_mock.get_request() + assert request.content == b"{}" + client.close() + + def test_calculate_retry_delay_uses_retry_after_seconds(self): + assert WorkOS._calculate_retry_delay(1, "30") == 30.0 + + def test_retry_exhaustion_raises_rate_limit(self, httpx_mock, monkeypatch): + monkeypatch.setattr(generated_client_module.time, "sleep", lambda _: None) + for _ in range(4): + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + client = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=3) + with pytest.raises(RateLimitExceededException): + client.request("GET", "test") + client.close() + + def test_rate_limit_retry_after_is_parsed(self, httpx_mock): + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "30"}, + json={"message": "Slow down"}, + ) + client = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + with pytest.raises(RateLimitExceededException) as exc_info: + client.request("GET", "test") + assert exc_info.value.retry_after == 30.0 + client.close() + + def test_timeout_error_is_wrapped(self, httpx_mock, monkeypatch): + monkeypatch.setattr(generated_client_module.time, "sleep", lambda _: None) + httpx_mock.add_exception(httpx.TimeoutException("timed out")) + client = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + with pytest.raises(generated_client_module.WorkOSTimeoutException): + client.request("GET", "test") + client.close() + + def test_connection_error_is_wrapped(self, httpx_mock, monkeypatch): + monkeypatch.setattr(generated_client_module.time, "sleep", lambda _: None) + httpx_mock.add_exception(httpx.ConnectError("connect failed")) + client = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + with pytest.raises(generated_client_module.WorkOSConnectionException): + client.request("GET", "test") + client.close() + + def test_documented_import_surface_exposes_resources(self): + client = WorkOS(api_key="sk_test_123", client_id="client_test") + assert client.admin_portal is not None + assert client.api_keys is not None + assert client.application_client_secrets is not None + assert client.applications is not None + assert client.audit_logs is not None + assert client.authorization is not None + assert client.connections is not None + assert client.directories is not None + assert client.directory_groups is not None + assert client.directory_users is not None + assert client.events is not None + assert client.feature_flags is not None + assert client.feature_flags.targets is not None + assert client.multi_factor_auth is not None + assert client.multi_factor_auth.challenges is not None + assert client.organization_domains is not None + assert client.organizations is not None + assert client.organizations.api_keys is not None + assert client.organizations.feature_flags is not None + assert client.permissions is not None + assert client.pipes is not None + assert client.radar is not None + assert client.sso is not None + assert client.user_management.authentication is not None + assert client.user_management.cors_origins is not None + assert client.user_management.data_providers is not None + assert client.user_management.invitations is not None + assert client.user_management.jwt_template is not None + assert client.user_management.magic_auth is not None + assert client.user_management.multi_factor_authentication is not None + assert client.user_management.organization_membership is not None + assert client.user_management.redirect_uris is not None + assert client.user_management.session_tokens is not None + assert client.user_management.users is not None + assert client.user_management_users.authorized_applications is not None + assert client.user_management_users.feature_flags is not None + assert client.webhooks is not None + assert client.widgets is not None + assert client.workos_connect is not None + client.close() + + +@pytest.mark.asyncio +class TestAsyncWorkOSClient: + async def test_documented_import_surface_exposes_resources(self): + client = AsyncWorkOS(api_key="sk_test_123", client_id="client_test") + assert client.admin_portal is not None + assert client.api_keys is not None + assert client.application_client_secrets is not None + assert client.applications is not None + assert client.audit_logs is not None + assert client.authorization is not None + assert client.connections is not None + assert client.directories is not None + assert client.directory_groups is not None + assert client.directory_users is not None + assert client.events is not None + assert client.feature_flags is not None + assert client.feature_flags.targets is not None + assert client.multi_factor_auth is not None + assert client.multi_factor_auth.challenges is not None + assert client.organization_domains is not None + assert client.organizations is not None + assert client.organizations.api_keys is not None + assert client.organizations.feature_flags is not None + assert client.permissions is not None + assert client.pipes is not None + assert client.radar is not None + assert client.sso is not None + assert client.user_management.authentication is not None + assert client.user_management.cors_origins is not None + assert client.user_management.data_providers is not None + assert client.user_management.invitations is not None + assert client.user_management.jwt_template is not None + assert client.user_management.magic_auth is not None + assert client.user_management.multi_factor_authentication is not None + assert client.user_management.organization_membership is not None + assert client.user_management.redirect_uris is not None + assert client.user_management.session_tokens is not None + assert client.user_management.users is not None + assert client.user_management_users.authorized_applications is not None + assert client.user_management_users.feature_flags is not None + assert client.webhooks is not None + assert client.widgets is not None + assert client.workos_connect is not None + await client.close() + + async def test_timeout_error_is_wrapped(self, httpx_mock, monkeypatch): + async def _sleep(_: float) -> None: + return None + + monkeypatch.setattr(generated_client_module.asyncio, "sleep", _sleep) + httpx_mock.add_exception(httpx.TimeoutException("timed out")) + client = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + with pytest.raises(generated_client_module.WorkOSTimeoutException): + await client.request("GET", "test") + await client.close() + + async def test_connection_error_is_wrapped(self, httpx_mock, monkeypatch): + async def _sleep(_: float) -> None: + return None + + monkeypatch.setattr(generated_client_module.asyncio, "sleep", _sleep) + httpx_mock.add_exception(httpx.ConnectError("connect failed")) + client = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + with pytest.raises(generated_client_module.WorkOSConnectionException): + await client.request("GET", "test") + await client.close() diff --git a/tests/test_models_round_trip.py b/tests/test_models_round_trip.py new file mode 100644 index 00000000..e12f32ec --- /dev/null +++ b/tests/test_models_round_trip.py @@ -0,0 +1,4479 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Model round-trip tests: from_dict(to_dict()) preserves data.""" + +from tests.generated_helpers import load_fixture + +from workos.admin_portal.models import ( + IntentOptions, + PortalLinkResponse, + SSOIntentOptions, +) +from workos.api_keys.models import ApiKey, ApiKeyOwner, ApiKeyValidationResponse +from workos.application_client_secrets.models import ( + ApplicationCredentialsListItem, + NewConnectApplicationSecret, +) +from workos.applications.models import ConnectApplication, RedirectUriDto +from workos.audit_logs.models import ( + AuditLogActionJson, + AuditLogEvent, + AuditLogEventActor, + AuditLogEventContext, + AuditLogEventCreateResponse, + AuditLogEventTarget, + AuditLogExportJson, + AuditLogSchemaActor, + AuditLogSchemaJson, + AuditLogSchemaJsonActor, + AuditLogSchemaJsonTarget, + AuditLogSchemaTarget, +) +from workos.authorization.models import ( + AuthorizationCheck, + AuthorizationResource, + ListData, + ListModel, + Role, + RoleAssignment, + RoleAssignmentResource, + RoleList, + SlimRole, + UserOrganizationMembershipBaseListData, +) +from workos.connections.models import Connection, ConnectionDomain, ConnectionOption +from workos.directories.models import ( + Directory, + DirectoryMetadata, + DirectoryMetadataUser, +) +from workos.directory_groups.models import DirectoryGroup +from workos.directory_users.models import ( + DirectoryUserWithGroups, + DirectoryUserWithGroupsEmail, +) +from workos.events.models import Event +from workos.feature_flags.models import FeatureFlag, FeatureFlagOwner, Flag, FlagOwner +from workos.multi_factor_auth.models import ( + AuthenticationFactor, + AuthenticationFactorEnrolled, + AuthenticationFactorEnrolledSms, + AuthenticationFactorEnrolledTotp, + AuthenticationFactorSms, + AuthenticationFactorTotp, +) +from workos.multi_factor_auth.challenges.models import ( + AuthenticationChallenge, + AuthenticationChallengeVerifyResponse, +) +from workos.organization_domains.models import ( + OrganizationDomain, + OrganizationDomainStandAlone, +) +from workos.organizations.models import ( + AuditLogConfiguration, + AuditLogConfigurationLogStream, + AuditLogsRetentionJson, + Organization, + OrganizationDomainData, +) +from workos.organizations.api_keys.models import ApiKeyWithValue, ApiKeyWithValueOwner +from workos.permissions.models import AuthorizationPermission, Permission +from workos.pipes.models import ( + DataIntegrationAccessTokenResponse, + DataIntegrationAuthorizeUrlResponse, +) +from workos.radar.models import ( + RadarListEntryAlreadyPresentResponse, + RadarStandaloneResponse, +) +from workos.sso.models import ( + Profile, + SSOAuthorizeUrlResponse, + SSOLogoutAuthorizeResponse, + SSOTokenResponse, + SSOTokenResponseOAuthToken, +) +from workos.user_management.authentication.models import ( + AuthenticateResponse, + AuthenticateResponseImpersonator, + AuthenticateResponseOAuthToken, + DeviceAuthorizationResponse, + User, +) +from workos.user_management.cors_origins.models import CORSOriginResponse +from workos.user_management.data_providers.models import ( + ConnectedAccount, + DataIntegrationsListResponse, + DataIntegrationsListResponseData, + DataIntegrationsListResponseDataConnectedAccount, +) +from workos.user_management.invitations.models import Invitation, UserInvite +from workos.user_management.jwt_template.models import JWTTemplateResponse +from workos.user_management.magic_auth.models import MagicAuth +from workos.user_management.multi_factor_authentication.models import ( + UserAuthenticationFactorEnrollResponse, +) +from workos.user_management.organization_membership.models import ( + OrganizationMembership, + UserOrganizationMembership, +) +from workos.user_management.redirect_uris.models import RedirectUri +from workos.user_management.session_tokens.models import JwksResponse, JwksResponseKeys +from workos.user_management.users.models import ( + EmailVerification, + PasswordReset, + ResetPasswordResponse, + SendVerificationEmailResponse, + UserIdentitiesGetItem, + UserSessionsImpersonator, + UserSessionsListItem, + VerifyEmailResponse, +) +from workos.user_management_users.authorized_applications.models import ( + AuthorizedConnectApplicationListData, +) +from workos.webhooks.models import WebhookEndpointJson +from workos.widgets.models import WidgetSessionTokenResponse +from workos.workos_connect.models import ( + ExternalAuthCompleteResponse, + UserConsentOption, + UserConsentOptionChoice, + UserObject, +) + + +class TestModelRoundTrip: + def test_user_object_round_trip(self): + data = load_fixture("user_object.json") + instance = UserObject.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = UserObject.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_user_object_minimal_payload(self): + data = {"id": "user_12345", "email": "marcelina.davis@example.com"} + instance = UserObject.from_dict(data) + serialized = instance.to_dict() + assert serialized["id"] == data["id"] + assert serialized["email"] == data["email"] + + def test_user_object_omits_absent_optional_non_nullable_fields(self): + data = {"id": "user_12345", "email": "marcelina.davis@example.com"} + instance = UserObject.from_dict(data) + serialized = instance.to_dict() + assert "first_name" not in serialized + assert "last_name" not in serialized + assert "metadata" not in serialized + + def test_user_consent_option_round_trip(self): + data = load_fixture("user_consent_option.json") + instance = UserConsentOption.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = UserConsentOption.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_user_consent_option_minimal_payload(self): + data = { + "claim": "tos_accepted", + "type": "enum", + "label": "Terms of Service", + "choices": [ + {"value": "accepted", "label": "I accept the Terms of Service"} + ], + } + instance = UserConsentOption.from_dict(data) + serialized = instance.to_dict() + assert serialized["claim"] == data["claim"] + assert serialized["type"] == data["type"] + assert serialized["label"] == data["label"] + assert serialized["choices"] == data["choices"] + + def test_redirect_uri_dto_round_trip(self): + data = load_fixture("redirect_uri_dto.json") + instance = RedirectUriDto.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = RedirectUriDto.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_redirect_uri_dto_minimal_payload(self): + data = {"uri": "https://example.com/callback"} + instance = RedirectUriDto.from_dict(data) + serialized = instance.to_dict() + assert serialized["uri"] == data["uri"] + + def test_redirect_uri_dto_preserves_nullable_fields(self): + data = {"uri": "https://example.com/callback", "default": None} + instance = RedirectUriDto.from_dict(data) + serialized = instance.to_dict() + assert serialized["default"] is None + + def test_audit_log_event_actor_round_trip(self): + data = load_fixture("audit_log_event_actor.json") + instance = AuditLogEventActor.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuditLogEventActor.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_audit_log_event_actor_minimal_payload(self): + data = {"id": "user_TF4C5938", "type": "user"} + instance = AuditLogEventActor.from_dict(data) + serialized = instance.to_dict() + assert serialized["id"] == data["id"] + assert serialized["type"] == data["type"] + + def test_audit_log_event_actor_omits_absent_optional_non_nullable_fields(self): + data = {"id": "user_TF4C5938", "type": "user"} + instance = AuditLogEventActor.from_dict(data) + serialized = instance.to_dict() + assert "name" not in serialized + assert "metadata" not in serialized + + def test_audit_log_event_target_round_trip(self): + data = load_fixture("audit_log_event_target.json") + instance = AuditLogEventTarget.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuditLogEventTarget.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_audit_log_event_target_minimal_payload(self): + data = {"id": "user_TF4C5938", "type": "user"} + instance = AuditLogEventTarget.from_dict(data) + serialized = instance.to_dict() + assert serialized["id"] == data["id"] + assert serialized["type"] == data["type"] + + def test_audit_log_event_target_omits_absent_optional_non_nullable_fields(self): + data = {"id": "user_TF4C5938", "type": "user"} + instance = AuditLogEventTarget.from_dict(data) + serialized = instance.to_dict() + assert "name" not in serialized + assert "metadata" not in serialized + + def test_audit_log_event_context_round_trip(self): + data = load_fixture("audit_log_event_context.json") + instance = AuditLogEventContext.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuditLogEventContext.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_audit_log_event_context_minimal_payload(self): + data = {"location": "123.123.123.123"} + instance = AuditLogEventContext.from_dict(data) + serialized = instance.to_dict() + assert serialized["location"] == data["location"] + + def test_audit_log_event_context_omits_absent_optional_non_nullable_fields(self): + data = {"location": "123.123.123.123"} + instance = AuditLogEventContext.from_dict(data) + serialized = instance.to_dict() + assert "user_agent" not in serialized + + def test_audit_log_event_round_trip(self): + data = load_fixture("audit_log_event.json") + instance = AuditLogEvent.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuditLogEvent.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_audit_log_event_minimal_payload(self): + data = { + "action": "user.signed_in", + "occurred_at": "2026-02-02T16:35:39.317Z", + "actor": { + "id": "user_TF4C5938", + "type": "user", + "name": "Jon Smith", + "metadata": {"owner": "user_01GBTCQ2"}, + }, + "targets": [ + { + "id": "user_TF4C5938", + "type": "user", + "name": "Jon Smith", + "metadata": {"owner": "user_01GBTCQ2"}, + } + ], + "context": { + "location": "123.123.123.123", + "user_agent": "Chrome/104.0.0.0", + }, + } + instance = AuditLogEvent.from_dict(data) + serialized = instance.to_dict() + assert serialized["action"] == data["action"] + assert serialized["occurred_at"] == data["occurred_at"] + assert serialized["actor"] == data["actor"] + assert serialized["targets"] == data["targets"] + assert serialized["context"] == data["context"] + + def test_audit_log_event_omits_absent_optional_non_nullable_fields(self): + data = { + "action": "user.signed_in", + "occurred_at": "2026-02-02T16:35:39.317Z", + "actor": { + "id": "user_TF4C5938", + "type": "user", + "name": "Jon Smith", + "metadata": {"owner": "user_01GBTCQ2"}, + }, + "targets": [ + { + "id": "user_TF4C5938", + "type": "user", + "name": "Jon Smith", + "metadata": {"owner": "user_01GBTCQ2"}, + } + ], + "context": { + "location": "123.123.123.123", + "user_agent": "Chrome/104.0.0.0", + }, + } + instance = AuditLogEvent.from_dict(data) + serialized = instance.to_dict() + assert "metadata" not in serialized + assert "version" not in serialized + + def test_audit_log_schema_actor_round_trip(self): + data = load_fixture("audit_log_schema_actor.json") + instance = AuditLogSchemaActor.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuditLogSchemaActor.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_audit_log_schema_actor_minimal_payload(self): + data = { + "metadata": {"type": "object", "properties": {"role": {"type": "string"}}} + } + instance = AuditLogSchemaActor.from_dict(data) + serialized = instance.to_dict() + assert serialized["metadata"] == data["metadata"] + + def test_audit_log_schema_target_round_trip(self): + data = load_fixture("audit_log_schema_target.json") + instance = AuditLogSchemaTarget.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuditLogSchemaTarget.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_audit_log_schema_target_minimal_payload(self): + data = {"type": "invoice"} + instance = AuditLogSchemaTarget.from_dict(data) + serialized = instance.to_dict() + assert serialized["type"] == data["type"] + + def test_audit_log_schema_target_omits_absent_optional_non_nullable_fields(self): + data = {"type": "invoice"} + instance = AuditLogSchemaTarget.from_dict(data) + serialized = instance.to_dict() + assert "metadata" not in serialized + + def test_organization_domain_data_round_trip(self): + data = load_fixture("organization_domain_data.json") + instance = OrganizationDomainData.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = OrganizationDomainData.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_organization_domain_data_minimal_payload(self): + data = {"domain": "foo-corp.com", "state": "verified"} + instance = OrganizationDomainData.from_dict(data) + serialized = instance.to_dict() + assert serialized["domain"] == data["domain"] + assert serialized["state"] == data["state"] + + def test_organization_domain_data_round_trips_unknown_enum_values(self): + data = { + "domain": "foo-corp.com", + "state": "unexpected_organization_domain_data_state", + } + instance = OrganizationDomainData.from_dict(data) + assert instance.to_dict() == data + + def test_sso_intent_options_round_trip(self): + data = load_fixture("sso_intent_options.json") + instance = SSOIntentOptions.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = SSOIntentOptions.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_sso_intent_options_minimal_payload(self): + data = {} + instance = SSOIntentOptions.from_dict(data) + assert instance.to_dict() is not None + + def test_sso_intent_options_omits_absent_optional_non_nullable_fields(self): + data = {} + instance = SSOIntentOptions.from_dict(data) + serialized = instance.to_dict() + assert "bookmark_slug" not in serialized + assert "provider_type" not in serialized + + def test_intent_options_round_trip(self): + data = load_fixture("intent_options.json") + instance = IntentOptions.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = IntentOptions.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_intent_options_minimal_payload(self): + data = {"sso": {"bookmark_slug": "chatgpt", "provider_type": "GoogleSAML"}} + instance = IntentOptions.from_dict(data) + serialized = instance.to_dict() + assert serialized["sso"] == data["sso"] + + def test_external_auth_complete_response_round_trip(self): + data = load_fixture("external_auth_complete_response.json") + instance = ExternalAuthCompleteResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = ExternalAuthCompleteResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_external_auth_complete_response_minimal_payload(self): + data = { + "redirect_uri": "https://your-authkit-domain.workos.com/oauth/authorize/complete?state=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdGF0ZSI6InJhbmRvbV9zdGF0ZV9zdHJpbmciLCJpYXQiOjE3NDI2MDQ4NTN9.abc123def456ghi789" + } + instance = ExternalAuthCompleteResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["redirect_uri"] == data["redirect_uri"] + + def test_api_key_round_trip(self): + data = load_fixture("api_key.json") + instance = ApiKey.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = ApiKey.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_api_key_minimal_payload(self): + data = { + "object": "api_key", + "id": "api_key_01EHZNVPK3SFK441A1RGBFSHRT", + "owner": {"type": "organization", "id": "org_01EHZNVPK3SFK441A1RGBFSHRT"}, + "name": "Production API Key", + "obfuscated_value": "sk_...3456", + "last_used_at": None, + "permissions": ["posts:read", "posts:write"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = ApiKey.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["owner"] == data["owner"] + assert serialized["name"] == data["name"] + assert serialized["obfuscated_value"] == data["obfuscated_value"] + assert serialized["last_used_at"] == data["last_used_at"] + assert serialized["permissions"] == data["permissions"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_api_key_preserves_nullable_fields(self): + data = { + "object": "api_key", + "id": "api_key_01EHZNVPK3SFK441A1RGBFSHRT", + "owner": {"type": "organization", "id": "org_01EHZNVPK3SFK441A1RGBFSHRT"}, + "name": "Production API Key", + "obfuscated_value": "sk_...3456", + "last_used_at": None, + "permissions": ["posts:read", "posts:write"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = ApiKey.from_dict(data) + serialized = instance.to_dict() + assert serialized["last_used_at"] is None + + def test_api_key_validation_response_round_trip(self): + data = load_fixture("api_key_validation_response.json") + instance = ApiKeyValidationResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = ApiKeyValidationResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_api_key_validation_response_minimal_payload(self): + data = {"api_key": None} + instance = ApiKeyValidationResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["api_key"] == data["api_key"] + + def test_api_key_validation_response_preserves_nullable_fields(self): + data = {"api_key": None} + instance = ApiKeyValidationResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["api_key"] is None + + def test_connect_application_round_trip(self): + data = load_fixture("connect_application.json") + instance = ConnectApplication.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = ConnectApplication.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_connect_application_minimal_payload(self): + data = { + "object": "connect_application", + "id": "conn_app_01HXYZ123456789ABCDEFGHIJ", + "client_id": "client_01HXYZ123456789ABCDEFGHIJ", + "description": None, + "name": "My Application", + "scopes": ["openid", "profile", "email"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = ConnectApplication.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["client_id"] == data["client_id"] + assert serialized["description"] == data["description"] + assert serialized["name"] == data["name"] + assert serialized["scopes"] == data["scopes"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_connect_application_preserves_nullable_fields(self): + data = { + "object": "connect_application", + "id": "conn_app_01HXYZ123456789ABCDEFGHIJ", + "client_id": "client_01HXYZ123456789ABCDEFGHIJ", + "description": None, + "name": "My Application", + "scopes": ["openid", "profile", "email"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = ConnectApplication.from_dict(data) + serialized = instance.to_dict() + assert serialized["description"] is None + + def test_new_connect_application_secret_round_trip(self): + data = load_fixture("new_connect_application_secret.json") + instance = NewConnectApplicationSecret.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = NewConnectApplicationSecret.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_new_connect_application_secret_minimal_payload(self): + data = { + "object": "connect_application_secret", + "id": "secret_01J9Q2Z3X4Y5W6V7U8T9S0R1Q", + "secret_hint": "abc123", + "last_used_at": None, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "secret": "abc123def456ghi789jkl012mno345pqr678stu901vwx234yz", + } + instance = NewConnectApplicationSecret.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["secret_hint"] == data["secret_hint"] + assert serialized["last_used_at"] == data["last_used_at"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + assert serialized["secret"] == data["secret"] + + def test_new_connect_application_secret_preserves_nullable_fields(self): + data = { + "object": "connect_application_secret", + "id": "secret_01J9Q2Z3X4Y5W6V7U8T9S0R1Q", + "secret_hint": "abc123", + "last_used_at": None, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "secret": "abc123def456ghi789jkl012mno345pqr678stu901vwx234yz", + } + instance = NewConnectApplicationSecret.from_dict(data) + serialized = instance.to_dict() + assert serialized["last_used_at"] is None + + def test_audit_log_event_create_response_round_trip(self): + data = load_fixture("audit_log_event_create_response.json") + instance = AuditLogEventCreateResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuditLogEventCreateResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_audit_log_event_create_response_minimal_payload(self): + data = {"success": True} + instance = AuditLogEventCreateResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["success"] == data["success"] + + def test_audit_log_export_json_round_trip(self): + data = load_fixture("audit_log_export_json.json") + instance = AuditLogExportJson.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuditLogExportJson.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_audit_log_export_json_minimal_payload(self): + data = { + "object": "audit_log_export", + "id": "audit_log_export_01GBZK5MP7TD1YCFQHFR22180V", + "state": "ready", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = AuditLogExportJson.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["state"] == data["state"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_audit_log_export_json_preserves_nullable_fields(self): + data = { + "object": "audit_log_export", + "id": "audit_log_export_01GBZK5MP7TD1YCFQHFR22180V", + "state": "ready", + "url": None, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = AuditLogExportJson.from_dict(data) + serialized = instance.to_dict() + assert serialized["url"] is None + + def test_audit_log_export_json_round_trips_unknown_enum_values(self): + data = { + "object": "audit_log_export", + "id": "audit_log_export_01GBZK5MP7TD1YCFQHFR22180V", + "state": "unexpected_audit_log_export_json_state", + "url": "https://exports.audit-logs.com/audit-log-exports/export.csv", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = AuditLogExportJson.from_dict(data) + assert instance.to_dict() == data + + def test_audit_logs_retention_json_round_trip(self): + data = load_fixture("audit_logs_retention_json.json") + instance = AuditLogsRetentionJson.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuditLogsRetentionJson.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_audit_logs_retention_json_minimal_payload(self): + data = {"retention_period_in_days": None} + instance = AuditLogsRetentionJson.from_dict(data) + serialized = instance.to_dict() + assert ( + serialized["retention_period_in_days"] == data["retention_period_in_days"] + ) + + def test_audit_logs_retention_json_preserves_nullable_fields(self): + data = {"retention_period_in_days": None} + instance = AuditLogsRetentionJson.from_dict(data) + serialized = instance.to_dict() + assert serialized["retention_period_in_days"] is None + + def test_audit_log_schema_json_round_trip(self): + data = load_fixture("audit_log_schema_json.json") + instance = AuditLogSchemaJson.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuditLogSchemaJson.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_audit_log_schema_json_minimal_payload(self): + data = { + "object": "audit_log_schema", + "version": 1, + "targets": [ + { + "type": "invoice", + "metadata": { + "type": "object", + "properties": {"cost": {"type": "number"}}, + }, + } + ], + "created_at": "2026-01-15T12:00:00.000Z", + } + instance = AuditLogSchemaJson.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["version"] == data["version"] + assert serialized["targets"] == data["targets"] + assert serialized["created_at"] == data["created_at"] + + def test_audit_log_schema_json_omits_absent_optional_non_nullable_fields(self): + data = { + "object": "audit_log_schema", + "version": 1, + "targets": [ + { + "type": "invoice", + "metadata": { + "type": "object", + "properties": {"cost": {"type": "number"}}, + }, + } + ], + "created_at": "2026-01-15T12:00:00.000Z", + } + instance = AuditLogSchemaJson.from_dict(data) + serialized = instance.to_dict() + assert "actor" not in serialized + assert "metadata" not in serialized + + def test_audit_log_action_json_round_trip(self): + data = load_fixture("audit_log_action_json.json") + instance = AuditLogActionJson.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuditLogActionJson.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_audit_log_action_json_minimal_payload(self): + data = { + "object": "audit_log_action", + "name": "user.viewed_invoice", + "schema": { + "object": "audit_log_schema", + "version": 1, + "actor": { + "metadata": { + "type": "object", + "properties": {"role": {"type": "string"}}, + } + }, + "targets": [ + { + "type": "invoice", + "metadata": { + "type": "object", + "properties": {"cost": {"type": "number"}}, + }, + } + ], + "metadata": { + "type": "object", + "properties": {"transactionId": {"type": "string"}}, + }, + "created_at": "2026-01-15T12:00:00.000Z", + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = AuditLogActionJson.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["name"] == data["name"] + assert serialized["schema"] == data["schema"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_authentication_challenge_round_trip(self): + data = load_fixture("authentication_challenge.json") + instance = AuthenticationChallenge.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuthenticationChallenge.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_authentication_challenge_minimal_payload(self): + data = { + "object": "authentication_challenge", + "id": "auth_challenge_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "authentication_factor_id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = AuthenticationChallenge.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert ( + serialized["authentication_factor_id"] == data["authentication_factor_id"] + ) + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_authentication_challenge_omits_absent_optional_non_nullable_fields(self): + data = { + "object": "authentication_challenge", + "id": "auth_challenge_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "authentication_factor_id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = AuthenticationChallenge.from_dict(data) + serialized = instance.to_dict() + assert "expires_at" not in serialized + assert "code" not in serialized + + def test_authentication_challenge_verify_response_round_trip(self): + data = load_fixture("authentication_challenge_verify_response.json") + instance = AuthenticationChallengeVerifyResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuthenticationChallengeVerifyResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_authentication_challenge_verify_response_minimal_payload(self): + data = { + "challenge": { + "object": "authentication_challenge", + "id": "auth_challenge_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "expires_at": "2026-01-15T12:00:00.000Z", + "code": "123456", + "authentication_factor_id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + }, + "valid": True, + } + instance = AuthenticationChallengeVerifyResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["challenge"] == data["challenge"] + assert serialized["valid"] == data["valid"] + + def test_authentication_factor_enrolled_round_trip(self): + data = load_fixture("authentication_factor_enrolled.json") + instance = AuthenticationFactorEnrolled.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuthenticationFactorEnrolled.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_authentication_factor_enrolled_minimal_payload(self): + data = { + "object": "authentication_factor", + "id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "type": "totp", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = AuthenticationFactorEnrolled.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["type"] == data["type"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_authentication_factor_enrolled_omits_absent_optional_non_nullable_fields( + self, + ): + data = { + "object": "authentication_factor", + "id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "type": "totp", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = AuthenticationFactorEnrolled.from_dict(data) + serialized = instance.to_dict() + assert "user_id" not in serialized + assert "sms" not in serialized + assert "totp" not in serialized + + def test_authentication_factor_enrolled_round_trips_unknown_enum_values(self): + data = { + "object": "authentication_factor", + "id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "type": "unexpected_authentication_factor_enrolled_type", + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "sms": {"phone_number": "+15005550006"}, + "totp": { + "issuer": "WorkOS", + "user": "user@example.com", + "secret": "JBSWY3DPEHPK3PXP", + "qr_code": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...", + "uri": "otpauth://totp/WorkOS:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=WorkOS", + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = AuthenticationFactorEnrolled.from_dict(data) + assert instance.to_dict() == data + + def test_authentication_factor_round_trip(self): + data = load_fixture("authentication_factor.json") + instance = AuthenticationFactor.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuthenticationFactor.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_authentication_factor_minimal_payload(self): + data = { + "object": "authentication_factor", + "id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "type": "totp", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = AuthenticationFactor.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["type"] == data["type"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_authentication_factor_omits_absent_optional_non_nullable_fields(self): + data = { + "object": "authentication_factor", + "id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "type": "totp", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = AuthenticationFactor.from_dict(data) + serialized = instance.to_dict() + assert "user_id" not in serialized + assert "sms" not in serialized + assert "totp" not in serialized + + def test_authentication_factor_round_trips_unknown_enum_values(self): + data = { + "object": "authentication_factor", + "id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "type": "unexpected_authentication_factor_type", + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "sms": {"phone_number": "+15005550006"}, + "totp": {"issuer": "WorkOS", "user": "user@example.com"}, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = AuthenticationFactor.from_dict(data) + assert instance.to_dict() == data + + def test_authorization_check_round_trip(self): + data = load_fixture("authorization_check.json") + instance = AuthorizationCheck.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuthorizationCheck.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_authorization_check_minimal_payload(self): + data = {"authorized": True} + instance = AuthorizationCheck.from_dict(data) + serialized = instance.to_dict() + assert serialized["authorized"] == data["authorized"] + + def test_authorization_resource_round_trip(self): + data = load_fixture("authorization_resource.json") + instance = AuthorizationResource.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuthorizationResource.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_authorization_resource_minimal_payload(self): + data = { + "object": "authorization_resource", + "name": "Website Redesign", + "description": None, + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "parent_resource_id": None, + "id": "authz_resource_01HXYZ123456789ABCDEFGH", + "external_id": "proj-456", + "resource_type_slug": "project", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = AuthorizationResource.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["name"] == data["name"] + assert serialized["description"] == data["description"] + assert serialized["organization_id"] == data["organization_id"] + assert serialized["parent_resource_id"] == data["parent_resource_id"] + assert serialized["id"] == data["id"] + assert serialized["external_id"] == data["external_id"] + assert serialized["resource_type_slug"] == data["resource_type_slug"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_authorization_resource_preserves_nullable_fields(self): + data = { + "object": "authorization_resource", + "name": "Website Redesign", + "description": None, + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "parent_resource_id": None, + "id": "authz_resource_01HXYZ123456789ABCDEFGH", + "external_id": "proj-456", + "resource_type_slug": "project", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = AuthorizationResource.from_dict(data) + serialized = instance.to_dict() + assert serialized["description"] is None + assert serialized["parent_resource_id"] is None + + def test_slim_role_round_trip(self): + data = load_fixture("slim_role.json") + instance = SlimRole.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = SlimRole.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_slim_role_minimal_payload(self): + data = {"slug": "admin"} + instance = SlimRole.from_dict(data) + serialized = instance.to_dict() + assert serialized["slug"] == data["slug"] + + def test_role_assignment_round_trip(self): + data = load_fixture("role_assignment.json") + instance = RoleAssignment.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = RoleAssignment.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_role_assignment_minimal_payload(self): + data = { + "object": "role_assignment", + "id": "role_assignment_01HXYZ123456789ABCDEFGH", + "role": {"slug": "admin"}, + "resource": { + "id": "authz_resource_01HXYZ123456789ABCDEFGH", + "external_id": "proj-456", + "resource_type_slug": "project", + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = RoleAssignment.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["role"] == data["role"] + assert serialized["resource"] == data["resource"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_role_round_trip(self): + data = load_fixture("role.json") + instance = Role.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = Role.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_role_minimal_payload(self): + data = { + "slug": "admin", + "object": "role", + "id": "role_01EHQMYV6MBK39QC5PZXHY59C3", + "name": "Admin", + "description": None, + "type": "EnvironmentRole", + "resource_type_slug": "default", + "permissions": ["posts:read", "posts:write"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = Role.from_dict(data) + serialized = instance.to_dict() + assert serialized["slug"] == data["slug"] + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["name"] == data["name"] + assert serialized["description"] == data["description"] + assert serialized["type"] == data["type"] + assert serialized["resource_type_slug"] == data["resource_type_slug"] + assert serialized["permissions"] == data["permissions"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_role_preserves_nullable_fields(self): + data = { + "slug": "admin", + "object": "role", + "id": "role_01EHQMYV6MBK39QC5PZXHY59C3", + "name": "Admin", + "description": None, + "type": "EnvironmentRole", + "resource_type_slug": "default", + "permissions": ["posts:read", "posts:write"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = Role.from_dict(data) + serialized = instance.to_dict() + assert serialized["description"] is None + + def test_role_round_trips_unknown_enum_values(self): + data = { + "slug": "admin", + "object": "role", + "id": "role_01EHQMYV6MBK39QC5PZXHY59C3", + "name": "Admin", + "description": "Can manage all resources", + "type": "unexpected_role_type", + "resource_type_slug": "default", + "permissions": ["posts:read", "posts:write"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = Role.from_dict(data) + assert instance.to_dict() == data + + def test_authorization_permission_round_trip(self): + data = load_fixture("authorization_permission.json") + instance = AuthorizationPermission.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuthorizationPermission.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_authorization_permission_minimal_payload(self): + data = { + "object": "permission", + "id": "perm_01HXYZ123456789ABCDEFGHIJ", + "slug": "documents:read", + "name": "View Documents", + "description": None, + "system": False, + "resource_type_slug": "workspace", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = AuthorizationPermission.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["slug"] == data["slug"] + assert serialized["name"] == data["name"] + assert serialized["description"] == data["description"] + assert serialized["system"] == data["system"] + assert serialized["resource_type_slug"] == data["resource_type_slug"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_authorization_permission_preserves_nullable_fields(self): + data = { + "object": "permission", + "id": "perm_01HXYZ123456789ABCDEFGHIJ", + "slug": "documents:read", + "name": "View Documents", + "description": None, + "system": False, + "resource_type_slug": "workspace", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = AuthorizationPermission.from_dict(data) + serialized = instance.to_dict() + assert serialized["description"] is None + + def test_role_list_round_trip(self): + data = load_fixture("role_list.json") + instance = RoleList.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = RoleList.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_role_list_minimal_payload(self): + data = { + "object": "list", + "data": [ + { + "slug": "admin", + "object": "role", + "id": "role_01EHQMYV6MBK39QC5PZXHY59C3", + "name": "Admin", + "description": "Can manage all resources", + "type": "EnvironmentRole", + "resource_type_slug": "default", + "permissions": ["posts:read", "posts:write"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + ], + } + instance = RoleList.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["data"] == data["data"] + + def test_connection_round_trip(self): + data = load_fixture("connection.json") + instance = Connection.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = Connection.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_connection_minimal_payload(self): + data = { + "object": "connection", + "id": "conn_01E4ZCR3C56J083X43JQXF3JK5", + "connection_type": "OktaSAML", + "name": "Foo Corp", + "state": "active", + "status": "linked", + "domains": [ + { + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "object": "connection_domain", + "domain": "foo-corp.com", + } + ], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = Connection.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["connection_type"] == data["connection_type"] + assert serialized["name"] == data["name"] + assert serialized["state"] == data["state"] + assert serialized["status"] == data["status"] + assert serialized["domains"] == data["domains"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_connection_omits_absent_optional_non_nullable_fields(self): + data = { + "object": "connection", + "id": "conn_01E4ZCR3C56J083X43JQXF3JK5", + "connection_type": "OktaSAML", + "name": "Foo Corp", + "state": "active", + "status": "linked", + "domains": [ + { + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "object": "connection_domain", + "domain": "foo-corp.com", + } + ], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = Connection.from_dict(data) + serialized = instance.to_dict() + assert "organization_id" not in serialized + assert "options" not in serialized + + def test_connection_round_trips_unknown_enum_values(self): + data = { + "object": "connection", + "id": "conn_01E4ZCR3C56J083X43JQXF3JK5", + "organization_id": "org_01EHWNCE74X7JSDV0X3SZ3KJNY", + "connection_type": "unexpected_connection_connection_type", + "name": "Foo Corp", + "state": "active", + "status": "linked", + "domains": [ + { + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "object": "connection_domain", + "domain": "foo-corp.com", + } + ], + "options": {"signing_cert": None}, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = Connection.from_dict(data) + assert instance.to_dict() == data + + def test_cors_origin_response_round_trip(self): + data = load_fixture("cors_origin_response.json") + instance = CORSOriginResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = CORSOriginResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_cors_origin_response_minimal_payload(self): + data = { + "object": "cors_origin", + "id": "cors_origin_01HXYZ123456789ABCDEFGHIJ", + "origin": "https://example.com", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = CORSOriginResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["origin"] == data["origin"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_directory_round_trip(self): + data = load_fixture("directory.json") + instance = Directory.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = Directory.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_directory_minimal_payload(self): + data = { + "object": "directory", + "id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "external_key": "sPa12dwRQ", + "type": "gsuite directory", + "state": "unlinked", + "name": "Foo Corp", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = Directory.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["organization_id"] == data["organization_id"] + assert serialized["external_key"] == data["external_key"] + assert serialized["type"] == data["type"] + assert serialized["state"] == data["state"] + assert serialized["name"] == data["name"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_directory_omits_absent_optional_non_nullable_fields(self): + data = { + "object": "directory", + "id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "external_key": "sPa12dwRQ", + "type": "gsuite directory", + "state": "unlinked", + "name": "Foo Corp", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = Directory.from_dict(data) + serialized = instance.to_dict() + assert "domain" not in serialized + assert "metadata" not in serialized + + def test_directory_round_trips_unknown_enum_values(self): + data = { + "object": "directory", + "id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "external_key": "sPa12dwRQ", + "type": "unexpected_directory_type", + "state": "unlinked", + "name": "Foo Corp", + "domain": "foo-corp.com", + "metadata": {"users": {"active": 42, "inactive": 3}, "groups": 5}, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = Directory.from_dict(data) + assert instance.to_dict() == data + + def test_directory_group_round_trip(self): + data = load_fixture("directory_group.json") + instance = DirectoryGroup.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = DirectoryGroup.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_directory_group_minimal_payload(self): + data = { + "object": "directory_group", + "id": "directory_group_01E1JJS84MFPPQ3G655FHTKX6Z", + "idp_id": "02grqrue4294w24", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", + "name": "Developers", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = DirectoryGroup.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["idp_id"] == data["idp_id"] + assert serialized["directory_id"] == data["directory_id"] + assert serialized["organization_id"] == data["organization_id"] + assert serialized["name"] == data["name"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_directory_group_omits_absent_optional_non_nullable_fields(self): + data = { + "object": "directory_group", + "id": "directory_group_01E1JJS84MFPPQ3G655FHTKX6Z", + "idp_id": "02grqrue4294w24", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", + "name": "Developers", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = DirectoryGroup.from_dict(data) + serialized = instance.to_dict() + assert "raw_attributes" not in serialized + + def test_directory_user_with_groups_round_trip(self): + data = load_fixture("directory_user_with_groups.json") + instance = DirectoryUserWithGroups.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = DirectoryUserWithGroups.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_directory_user_with_groups_minimal_payload(self): + data = { + "object": "directory_user", + "id": "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", + "idp_id": "2836", + "email": None, + "state": "active", + "raw_attributes": {"key": {}}, + "custom_attributes": { + "department": "Engineering", + "job_title": "Software Engineer", + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "groups": [ + { + "object": "directory_group", + "id": "directory_group_01E1JJS84MFPPQ3G655FHTKX6Z", + "idp_id": "02grqrue4294w24", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", + "name": "Developers", + "raw_attributes": {"key": {}}, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + ], + } + instance = DirectoryUserWithGroups.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["directory_id"] == data["directory_id"] + assert serialized["organization_id"] == data["organization_id"] + assert serialized["idp_id"] == data["idp_id"] + assert serialized["email"] == data["email"] + assert serialized["state"] == data["state"] + assert serialized["raw_attributes"] == data["raw_attributes"] + assert serialized["custom_attributes"] == data["custom_attributes"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + assert serialized["groups"] == data["groups"] + + def test_directory_user_with_groups_omits_absent_optional_non_nullable_fields(self): + data = { + "object": "directory_user", + "id": "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", + "idp_id": "2836", + "email": "marcelina.davis@example.com", + "first_name": "Marcelina", + "last_name": "Davis", + "job_title": "Software Engineer", + "username": "mdavis", + "state": "active", + "raw_attributes": {"key": {}}, + "custom_attributes": { + "department": "Engineering", + "job_title": "Software Engineer", + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "groups": [ + { + "object": "directory_group", + "id": "directory_group_01E1JJS84MFPPQ3G655FHTKX6Z", + "idp_id": "02grqrue4294w24", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", + "name": "Developers", + "raw_attributes": {"key": {}}, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + ], + } + instance = DirectoryUserWithGroups.from_dict(data) + serialized = instance.to_dict() + assert "emails" not in serialized + assert "role" not in serialized + assert "roles" not in serialized + + def test_directory_user_with_groups_preserves_nullable_fields(self): + data = { + "object": "directory_user", + "id": "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", + "idp_id": "2836", + "email": None, + "first_name": None, + "last_name": None, + "emails": [ + { + "primary": True, + "type": "work", + "value": "marcelina.davis@example.com", + } + ], + "job_title": None, + "username": None, + "state": "active", + "raw_attributes": {"key": {}}, + "custom_attributes": { + "department": "Engineering", + "job_title": "Software Engineer", + }, + "role": {"slug": "admin"}, + "roles": [{"slug": "admin"}], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "groups": [ + { + "object": "directory_group", + "id": "directory_group_01E1JJS84MFPPQ3G655FHTKX6Z", + "idp_id": "02grqrue4294w24", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", + "name": "Developers", + "raw_attributes": {"key": {}}, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + ], + } + instance = DirectoryUserWithGroups.from_dict(data) + serialized = instance.to_dict() + assert serialized["email"] is None + assert serialized["first_name"] is None + assert serialized["last_name"] is None + assert serialized["job_title"] is None + assert serialized["username"] is None + + def test_directory_user_with_groups_round_trips_unknown_enum_values(self): + data = { + "object": "directory_user", + "id": "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", + "idp_id": "2836", + "email": "marcelina.davis@example.com", + "first_name": "Marcelina", + "last_name": "Davis", + "emails": [ + { + "primary": True, + "type": "work", + "value": "marcelina.davis@example.com", + } + ], + "job_title": "Software Engineer", + "username": "mdavis", + "state": "unexpected_directory_user_with_groups_state", + "raw_attributes": {"key": {}}, + "custom_attributes": { + "department": "Engineering", + "job_title": "Software Engineer", + }, + "role": {"slug": "admin"}, + "roles": [{"slug": "admin"}], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "groups": [ + { + "object": "directory_group", + "id": "directory_group_01E1JJS84MFPPQ3G655FHTKX6Z", + "idp_id": "02grqrue4294w24", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", + "name": "Developers", + "raw_attributes": {"key": {}}, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + ], + } + instance = DirectoryUserWithGroups.from_dict(data) + assert instance.to_dict() == data + + def test_event_round_trip(self): + data = load_fixture("event.json") + instance = Event.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = Event.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_event_minimal_payload(self): + data = { + "object": "event", + "id": "event_01EHZNVPK3SFK441A1RGBFSHRT", + "event": "dsync.user.created", + "data": { + "id": "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ", + "state": "active", + "emails": [{"primary": True, "value": "veda@foo-corp.com"}], + "idp_id": "2836", + "object": "directory_user", + "username": "veda@foo-corp.com", + "last_name": "Torp", + "first_name": "Veda", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "raw_attributes": {}, + "custom_attributes": {}, + "created_at": "2021-06-25T19:07:33.155Z", + "updated_at": "2021-06-25T19:07:33.155Z", + }, + "created_at": "2026-01-15T12:00:00.000Z", + } + instance = Event.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["event"] == data["event"] + assert serialized["data"] == data["data"] + assert serialized["created_at"] == data["created_at"] + + def test_event_omits_absent_optional_non_nullable_fields(self): + data = { + "object": "event", + "id": "event_01EHZNVPK3SFK441A1RGBFSHRT", + "event": "dsync.user.created", + "data": { + "id": "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ", + "state": "active", + "emails": [{"primary": True, "value": "veda@foo-corp.com"}], + "idp_id": "2836", + "object": "directory_user", + "username": "veda@foo-corp.com", + "last_name": "Torp", + "first_name": "Veda", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "raw_attributes": {}, + "custom_attributes": {}, + "created_at": "2021-06-25T19:07:33.155Z", + "updated_at": "2021-06-25T19:07:33.155Z", + }, + "created_at": "2026-01-15T12:00:00.000Z", + } + instance = Event.from_dict(data) + serialized = instance.to_dict() + assert "context" not in serialized + + def test_jwt_template_response_round_trip(self): + data = load_fixture("jwt_template_response.json") + instance = JWTTemplateResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = JWTTemplateResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_jwt_template_response_minimal_payload(self): + data = { + "object": "jwt_template", + "content": '{"iss": "{{environment.id}}", "sub": "{{user.id}}"}', + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = JWTTemplateResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["content"] == data["content"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_organization_domain_stand_alone_round_trip(self): + data = load_fixture("organization_domain_stand_alone.json") + instance = OrganizationDomainStandAlone.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = OrganizationDomainStandAlone.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_organization_domain_stand_alone_minimal_payload(self): + data = { + "object": "organization_domain", + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "organization_id": "org_01HE8GSH8FQPASKSY27THRKRBP", + "domain": "foo-corp.com", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = OrganizationDomainStandAlone.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["organization_id"] == data["organization_id"] + assert serialized["domain"] == data["domain"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_organization_domain_stand_alone_omits_absent_optional_non_nullable_fields( + self, + ): + data = { + "object": "organization_domain", + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "organization_id": "org_01HE8GSH8FQPASKSY27THRKRBP", + "domain": "foo-corp.com", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = OrganizationDomainStandAlone.from_dict(data) + serialized = instance.to_dict() + assert "state" not in serialized + assert "verification_prefix" not in serialized + assert "verification_token" not in serialized + assert "verification_strategy" not in serialized + + def test_organization_domain_stand_alone_round_trips_unknown_enum_values(self): + data = { + "object": "organization_domain", + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "organization_id": "org_01HE8GSH8FQPASKSY27THRKRBP", + "domain": "foo-corp.com", + "state": "unexpected_organization_domain_stand_alone_state", + "verification_prefix": "superapp-domain-verification-z3kjny", + "verification_token": "m5Oztg3jdK4NJLgs8uIlIprMw", + "verification_strategy": "dns", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = OrganizationDomainStandAlone.from_dict(data) + assert instance.to_dict() == data + + def test_flag_round_trip(self): + data = load_fixture("flag.json") + instance = Flag.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = Flag.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_flag_minimal_payload(self): + data = { + "object": "feature_flag", + "id": "flag_01EHZNVPK3SFK441A1RGBFSHRT", + "slug": "advanced-analytics", + "name": "Advanced Analytics", + "description": None, + "owner": None, + "tags": ["reports"], + "enabled": True, + "default_value": False, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = Flag.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["slug"] == data["slug"] + assert serialized["name"] == data["name"] + assert serialized["description"] == data["description"] + assert serialized["owner"] == data["owner"] + assert serialized["tags"] == data["tags"] + assert serialized["enabled"] == data["enabled"] + assert serialized["default_value"] == data["default_value"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_flag_preserves_nullable_fields(self): + data = { + "object": "feature_flag", + "id": "flag_01EHZNVPK3SFK441A1RGBFSHRT", + "slug": "advanced-analytics", + "name": "Advanced Analytics", + "description": None, + "owner": None, + "tags": ["reports"], + "enabled": True, + "default_value": False, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = Flag.from_dict(data) + serialized = instance.to_dict() + assert serialized["description"] is None + assert serialized["owner"] is None + + def test_api_key_with_value_round_trip(self): + data = load_fixture("api_key_with_value.json") + instance = ApiKeyWithValue.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = ApiKeyWithValue.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_api_key_with_value_minimal_payload(self): + data = { + "object": "api_key", + "id": "api_key_01EHZNVPK3SFK441A1RGBFSHRT", + "owner": {"type": "organization", "id": "org_01EHZNVPK3SFK441A1RGBFSHRT"}, + "name": "Production API Key", + "obfuscated_value": "sk_...3456", + "last_used_at": None, + "permissions": ["posts:read", "posts:write"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "value": "sk_abcdefghijklmnop123456", + } + instance = ApiKeyWithValue.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["owner"] == data["owner"] + assert serialized["name"] == data["name"] + assert serialized["obfuscated_value"] == data["obfuscated_value"] + assert serialized["last_used_at"] == data["last_used_at"] + assert serialized["permissions"] == data["permissions"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + assert serialized["value"] == data["value"] + + def test_api_key_with_value_preserves_nullable_fields(self): + data = { + "object": "api_key", + "id": "api_key_01EHZNVPK3SFK441A1RGBFSHRT", + "owner": {"type": "organization", "id": "org_01EHZNVPK3SFK441A1RGBFSHRT"}, + "name": "Production API Key", + "obfuscated_value": "sk_...3456", + "last_used_at": None, + "permissions": ["posts:read", "posts:write"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "value": "sk_abcdefghijklmnop123456", + } + instance = ApiKeyWithValue.from_dict(data) + serialized = instance.to_dict() + assert serialized["last_used_at"] is None + + def test_organization_round_trip(self): + data = load_fixture("organization.json") + instance = Organization.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = Organization.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_organization_minimal_payload(self): + data = { + "object": "organization", + "id": "org_01EHWNCE74X7JSDV0X3SZ3KJNY", + "name": "Acme Inc.", + "domains": [ + { + "object": "organization_domain", + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "organization_id": "org_01HE8GSH8FQPASKSY27THRKRBP", + "domain": "foo-corp.com", + "state": "pending", + "verification_prefix": "superapp-domain-verification-z3kjny", + "verification_token": "m5Oztg3jdK4NJLgs8uIlIprMw", + "verification_strategy": "dns", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + ], + "metadata": {"tier": "diamond"}, + "external_id": None, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "allow_profiles_outside_organization": False, + } + instance = Organization.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["name"] == data["name"] + assert serialized["domains"] == data["domains"] + assert serialized["metadata"] == data["metadata"] + assert serialized["external_id"] == data["external_id"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + assert ( + serialized["allow_profiles_outside_organization"] + == data["allow_profiles_outside_organization"] + ) + + def test_organization_omits_absent_optional_non_nullable_fields(self): + data = { + "object": "organization", + "id": "org_01EHWNCE74X7JSDV0X3SZ3KJNY", + "name": "Acme Inc.", + "domains": [ + { + "object": "organization_domain", + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "organization_id": "org_01HE8GSH8FQPASKSY27THRKRBP", + "domain": "foo-corp.com", + "state": "pending", + "verification_prefix": "superapp-domain-verification-z3kjny", + "verification_token": "m5Oztg3jdK4NJLgs8uIlIprMw", + "verification_strategy": "dns", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + ], + "metadata": {"tier": "diamond"}, + "external_id": "2fe01467-f7ea-4dd2-8b79-c2b4f56d0191", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "allow_profiles_outside_organization": False, + } + instance = Organization.from_dict(data) + serialized = instance.to_dict() + assert "stripe_customer_id" not in serialized + + def test_organization_preserves_nullable_fields(self): + data = { + "object": "organization", + "id": "org_01EHWNCE74X7JSDV0X3SZ3KJNY", + "name": "Acme Inc.", + "domains": [ + { + "object": "organization_domain", + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "organization_id": "org_01HE8GSH8FQPASKSY27THRKRBP", + "domain": "foo-corp.com", + "state": "pending", + "verification_prefix": "superapp-domain-verification-z3kjny", + "verification_token": "m5Oztg3jdK4NJLgs8uIlIprMw", + "verification_strategy": "dns", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + ], + "metadata": {"tier": "diamond"}, + "external_id": None, + "stripe_customer_id": "cus_R9qWAGMQ6nGE7V", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "allow_profiles_outside_organization": False, + } + instance = Organization.from_dict(data) + serialized = instance.to_dict() + assert serialized["external_id"] is None + + def test_audit_log_configuration_round_trip(self): + data = load_fixture("audit_log_configuration.json") + instance = AuditLogConfiguration.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuditLogConfiguration.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_audit_log_configuration_minimal_payload(self): + data = { + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "retention_period_in_days": 30, + "state": "active", + } + instance = AuditLogConfiguration.from_dict(data) + serialized = instance.to_dict() + assert serialized["organization_id"] == data["organization_id"] + assert ( + serialized["retention_period_in_days"] == data["retention_period_in_days"] + ) + assert serialized["state"] == data["state"] + + def test_audit_log_configuration_omits_absent_optional_non_nullable_fields(self): + data = { + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "retention_period_in_days": 30, + "state": "active", + } + instance = AuditLogConfiguration.from_dict(data) + serialized = instance.to_dict() + assert "log_stream" not in serialized + + def test_audit_log_configuration_round_trips_unknown_enum_values(self): + data = { + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "retention_period_in_days": 30, + "state": "unexpected_audit_log_configuration_state", + "log_stream": { + "id": "als_01EHZNVPK3SFK441A1RGBFSHRT", + "type": "Datadog", + "state": "active", + "last_synced_at": "2026-01-15T12:00:00.000Z", + "created_at": "2026-01-15T12:00:00.000Z", + }, + } + instance = AuditLogConfiguration.from_dict(data) + assert instance.to_dict() == data + + def test_data_integration_authorize_url_response_round_trip(self): + data = load_fixture("data_integration_authorize_url_response.json") + instance = DataIntegrationAuthorizeUrlResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = DataIntegrationAuthorizeUrlResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_data_integration_authorize_url_response_minimal_payload(self): + data = { + "url": "https://api.workos.com/data-integrations/q2czJKmVAraSBg8xFpT7M9uR/authorize-redirect" + } + instance = DataIntegrationAuthorizeUrlResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["url"] == data["url"] + + def test_data_integration_access_token_response_round_trip(self): + data = load_fixture("data_integration_access_token_response.json") + instance = DataIntegrationAccessTokenResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = DataIntegrationAccessTokenResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_data_integration_access_token_response_minimal_payload(self): + data = {} + instance = DataIntegrationAccessTokenResponse.from_dict(data) + assert instance.to_dict() is not None + + def test_connected_account_round_trip(self): + data = load_fixture("connected_account.json") + instance = ConnectedAccount.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = ConnectedAccount.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_connected_account_minimal_payload(self): + data = { + "object": "connected_account", + "id": "data_installation_01EHZNVPK3SFK441A1RGBFSHRT", + "user_id": None, + "organization_id": None, + "scopes": ["repo", "user:email"], + "state": "connected", + "created_at": "2024-01-16T14:20:00.000Z", + "updated_at": "2024-01-16T14:20:00.000Z", + } + instance = ConnectedAccount.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["user_id"] == data["user_id"] + assert serialized["organization_id"] == data["organization_id"] + assert serialized["scopes"] == data["scopes"] + assert serialized["state"] == data["state"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_connected_account_preserves_nullable_fields(self): + data = { + "object": "connected_account", + "id": "data_installation_01EHZNVPK3SFK441A1RGBFSHRT", + "user_id": None, + "organization_id": None, + "scopes": ["repo", "user:email"], + "state": "connected", + "created_at": "2024-01-16T14:20:00.000Z", + "updated_at": "2024-01-16T14:20:00.000Z", + } + instance = ConnectedAccount.from_dict(data) + serialized = instance.to_dict() + assert serialized["user_id"] is None + assert serialized["organization_id"] is None + + def test_connected_account_round_trips_unknown_enum_values(self): + data = { + "object": "connected_account", + "id": "data_installation_01EHZNVPK3SFK441A1RGBFSHRT", + "user_id": "user_01EHZNVPK3SFK441A1RGBFSHRT", + "organization_id": None, + "scopes": ["repo", "user:email"], + "state": "unexpected_connected_account_state", + "created_at": "2024-01-16T14:20:00.000Z", + "updated_at": "2024-01-16T14:20:00.000Z", + } + instance = ConnectedAccount.from_dict(data) + assert instance.to_dict() == data + + def test_data_integrations_list_response_round_trip(self): + data = load_fixture("data_integrations_list_response.json") + instance = DataIntegrationsListResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = DataIntegrationsListResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_data_integrations_list_response_minimal_payload(self): + data = { + "object": "list", + "data": [ + { + "object": "data_provider", + "id": "data_integration_01EHZNVPK3SFK441A1RGBFSHRT", + "name": "GitHub", + "description": "Connect your GitHub account to access repositories.", + "slug": "github", + "integration_type": "github", + "credentials_type": "oauth2", + "scopes": ["repo", "user:email"], + "ownership": "userland_user", + "created_at": "2024-01-15T10:30:00.000Z", + "updated_at": "2024-01-15T10:30:00.000Z", + "connected_account": { + "object": "connected_account", + "id": "data_installation_01EHZNVPK3SFK441A1RGBFSHRT", + "user_id": "user_01EHZNVPK3SFK441A1RGBFSHRT", + "organization_id": None, + "scopes": ["repo", "user:email"], + "state": "connected", + "created_at": "2024-01-16T14:20:00.000Z", + "updated_at": "2024-01-16T14:20:00.000Z", + "userlandUserId": "test_userlandUserId", + }, + } + ], + } + instance = DataIntegrationsListResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["data"] == data["data"] + + def test_portal_link_response_round_trip(self): + data = load_fixture("portal_link_response.json") + instance = PortalLinkResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = PortalLinkResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_portal_link_response_minimal_payload(self): + data = { + "link": "https://setup.workos.com?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + } + instance = PortalLinkResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["link"] == data["link"] + + def test_radar_standalone_response_round_trip(self): + data = load_fixture("radar_standalone_response.json") + instance = RadarStandaloneResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = RadarStandaloneResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_radar_standalone_response_minimal_payload(self): + data = { + "verdict": "block", + "reason": "Detected enabled Radar control", + "attempt_id": "radar_att_01HZBC6N1EB1ZY7KG32X", + } + instance = RadarStandaloneResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["verdict"] == data["verdict"] + assert serialized["reason"] == data["reason"] + assert serialized["attempt_id"] == data["attempt_id"] + + def test_radar_standalone_response_omits_absent_optional_non_nullable_fields(self): + data = { + "verdict": "block", + "reason": "Detected enabled Radar control", + "attempt_id": "radar_att_01HZBC6N1EB1ZY7KG32X", + } + instance = RadarStandaloneResponse.from_dict(data) + serialized = instance.to_dict() + assert "control" not in serialized + assert "blocklist_type" not in serialized + + def test_radar_standalone_response_round_trips_unknown_enum_values(self): + data = { + "verdict": "unexpected_radar_standalone_response_verdict", + "reason": "Detected enabled Radar control", + "attempt_id": "radar_att_01HZBC6N1EB1ZY7KG32X", + "control": "bot_detection", + "blocklist_type": "ip_address", + } + instance = RadarStandaloneResponse.from_dict(data) + assert instance.to_dict() == data + + def test_radar_list_entry_already_present_response_round_trip(self): + data = load_fixture("radar_list_entry_already_present_response.json") + instance = RadarListEntryAlreadyPresentResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = RadarListEntryAlreadyPresentResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_radar_list_entry_already_present_response_minimal_payload(self): + data = {"message": "Entry already present in list"} + instance = RadarListEntryAlreadyPresentResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["message"] == data["message"] + + def test_redirect_uri_round_trip(self): + data = load_fixture("redirect_uri.json") + instance = RedirectUri.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = RedirectUri.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_redirect_uri_minimal_payload(self): + data = { + "object": "redirect_uri", + "id": "ruri_01EHZNVPK3SFK441A1RGBFSHRT", + "uri": "https://example.com/callback", + "default": True, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = RedirectUri.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["uri"] == data["uri"] + assert serialized["default"] == data["default"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_user_authentication_factor_enroll_response_round_trip(self): + data = load_fixture("user_authentication_factor_enroll_response.json") + instance = UserAuthenticationFactorEnrollResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = UserAuthenticationFactorEnrollResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_user_authentication_factor_enroll_response_minimal_payload(self): + data = { + "authentication_factor": { + "object": "authentication_factor", + "id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "type": "totp", + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "sms": {"phone_number": "+15005550006"}, + "totp": { + "issuer": "WorkOS", + "user": "user@example.com", + "secret": "JBSWY3DPEHPK3PXP", + "qr_code": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...", + "uri": "otpauth://totp/WorkOS:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=WorkOS", + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + }, + "authentication_challenge": { + "object": "authentication_challenge", + "id": "auth_challenge_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "expires_at": "2026-01-15T12:00:00.000Z", + "code": "123456", + "authentication_factor_id": "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + }, + } + instance = UserAuthenticationFactorEnrollResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["authentication_factor"] == data["authentication_factor"] + assert ( + serialized["authentication_challenge"] == data["authentication_challenge"] + ) + + def test_magic_auth_round_trip(self): + data = load_fixture("magic_auth.json") + instance = MagicAuth.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = MagicAuth.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_magic_auth_minimal_payload(self): + data = { + "object": "magic_auth", + "id": "magic_auth_01HWZBQZY2M3AMQW166Q22K88F", + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "expires_at": "2026-01-15T12:00:00.000Z", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "code": "123456", + } + instance = MagicAuth.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["user_id"] == data["user_id"] + assert serialized["email"] == data["email"] + assert serialized["expires_at"] == data["expires_at"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + assert serialized["code"] == data["code"] + + def test_user_invite_round_trip(self): + data = load_fixture("user_invite.json") + instance = UserInvite.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = UserInvite.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_user_invite_minimal_payload(self): + data = { + "object": "invitation", + "id": "invitation_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "state": "pending", + "accepted_at": None, + "revoked_at": None, + "expires_at": "2026-01-15T12:00:00.000Z", + "organization_id": None, + "inviter_user_id": None, + "accepted_user_id": None, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "token": "Z1uX3RbwcIl5fIGJJJCXXisdI", + "accept_invitation_url": "https://your-app.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI", + } + instance = UserInvite.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["email"] == data["email"] + assert serialized["state"] == data["state"] + assert serialized["accepted_at"] == data["accepted_at"] + assert serialized["revoked_at"] == data["revoked_at"] + assert serialized["expires_at"] == data["expires_at"] + assert serialized["organization_id"] == data["organization_id"] + assert serialized["inviter_user_id"] == data["inviter_user_id"] + assert serialized["accepted_user_id"] == data["accepted_user_id"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + assert serialized["token"] == data["token"] + assert serialized["accept_invitation_url"] == data["accept_invitation_url"] + + def test_user_invite_preserves_nullable_fields(self): + data = { + "object": "invitation", + "id": "invitation_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "state": "pending", + "accepted_at": None, + "revoked_at": None, + "expires_at": "2026-01-15T12:00:00.000Z", + "organization_id": None, + "inviter_user_id": None, + "accepted_user_id": None, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "token": "Z1uX3RbwcIl5fIGJJJCXXisdI", + "accept_invitation_url": "https://your-app.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI", + } + instance = UserInvite.from_dict(data) + serialized = instance.to_dict() + assert serialized["accepted_at"] is None + assert serialized["revoked_at"] is None + assert serialized["organization_id"] is None + assert serialized["inviter_user_id"] is None + assert serialized["accepted_user_id"] is None + + def test_user_invite_round_trips_unknown_enum_values(self): + data = { + "object": "invitation", + "id": "invitation_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "state": "unexpected_user_invite_state", + "accepted_at": None, + "revoked_at": None, + "expires_at": "2026-01-15T12:00:00.000Z", + "organization_id": "org_01E4ZCR3C56J083X43JQXF3JK5", + "inviter_user_id": "user_01HYGBX8ZGD19949T3BM4FW1C3", + "accepted_user_id": None, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "token": "Z1uX3RbwcIl5fIGJJJCXXisdI", + "accept_invitation_url": "https://your-app.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI", + } + instance = UserInvite.from_dict(data) + assert instance.to_dict() == data + + def test_user_organization_membership_round_trip(self): + data = load_fixture("user_organization_membership.json") + instance = UserOrganizationMembership.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = UserOrganizationMembership.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_user_organization_membership_minimal_payload(self): + data = { + "object": "organization_membership", + "id": "om_01HXYZ123456789ABCDEFGHIJ", + "user_id": "user_01EHQTV6MWP9P1F4ZXGXMC8ABB", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "status": "active", + "directory_managed": False, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "role": {"slug": "admin"}, + } + instance = UserOrganizationMembership.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["user_id"] == data["user_id"] + assert serialized["organization_id"] == data["organization_id"] + assert serialized["status"] == data["status"] + assert serialized["directory_managed"] == data["directory_managed"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + assert serialized["role"] == data["role"] + + def test_user_organization_membership_omits_absent_optional_non_nullable_fields( + self, + ): + data = { + "object": "organization_membership", + "id": "om_01HXYZ123456789ABCDEFGHIJ", + "user_id": "user_01EHQTV6MWP9P1F4ZXGXMC8ABB", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "status": "active", + "directory_managed": False, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "role": {"slug": "admin"}, + } + instance = UserOrganizationMembership.from_dict(data) + serialized = instance.to_dict() + assert "organization_name" not in serialized + assert "custom_attributes" not in serialized + + def test_user_organization_membership_round_trips_unknown_enum_values(self): + data = { + "object": "organization_membership", + "id": "om_01HXYZ123456789ABCDEFGHIJ", + "user_id": "user_01EHQTV6MWP9P1F4ZXGXMC8ABB", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "status": "unexpected_user_organization_membership_status", + "directory_managed": False, + "organization_name": "Acme Corp", + "custom_attributes": { + "department": "Engineering", + "title": "Developer Experience Engineer", + "location": "Brooklyn", + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "role": {"slug": "admin"}, + } + instance = UserOrganizationMembership.from_dict(data) + assert instance.to_dict() == data + + def test_user_round_trip(self): + data = load_fixture("user.json") + instance = User.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = User.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_user_minimal_payload(self): + data = { + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": None, + "last_name": None, + "profile_picture_url": None, + "email": "marcelina.davis@example.com", + "email_verified": True, + "external_id": None, + "last_sign_in_at": None, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = User.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["first_name"] == data["first_name"] + assert serialized["last_name"] == data["last_name"] + assert serialized["profile_picture_url"] == data["profile_picture_url"] + assert serialized["email"] == data["email"] + assert serialized["email_verified"] == data["email_verified"] + assert serialized["external_id"] == data["external_id"] + assert serialized["last_sign_in_at"] == data["last_sign_in_at"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_user_omits_absent_optional_non_nullable_fields(self): + data = { + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": "Marcelina", + "last_name": "Davis", + "profile_picture_url": "https://workoscdn.com/images/v1/123abc", + "email": "marcelina.davis@example.com", + "email_verified": True, + "external_id": "f1ffa2b2-c20b-4d39-be5c-212726e11222", + "last_sign_in_at": "2025-06-25T19:07:33.155Z", + "locale": "en-US", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = User.from_dict(data) + serialized = instance.to_dict() + assert "metadata" not in serialized + + def test_user_preserves_nullable_fields(self): + data = { + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": None, + "last_name": None, + "profile_picture_url": None, + "email": "marcelina.davis@example.com", + "email_verified": True, + "external_id": None, + "metadata": {"timezone": "America/New_York"}, + "last_sign_in_at": None, + "locale": None, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = User.from_dict(data) + serialized = instance.to_dict() + assert serialized["first_name"] is None + assert serialized["last_name"] is None + assert serialized["profile_picture_url"] is None + assert serialized["external_id"] is None + assert serialized["last_sign_in_at"] is None + assert serialized["locale"] is None + + def test_email_verification_round_trip(self): + data = load_fixture("email_verification.json") + instance = EmailVerification.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = EmailVerification.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_email_verification_minimal_payload(self): + data = { + "object": "email_verification", + "id": "email_verification_01E4ZCR3C56J083X43JQXF3JK5", + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "expires_at": "2026-01-15T12:00:00.000Z", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "code": "123456", + } + instance = EmailVerification.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["user_id"] == data["user_id"] + assert serialized["email"] == data["email"] + assert serialized["expires_at"] == data["expires_at"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + assert serialized["code"] == data["code"] + + def test_send_verification_email_response_round_trip(self): + data = load_fixture("send_verification_email_response.json") + instance = SendVerificationEmailResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = SendVerificationEmailResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_send_verification_email_response_minimal_payload(self): + data = { + "user": { + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": "Marcelina", + "last_name": "Davis", + "profile_picture_url": "https://workoscdn.com/images/v1/123abc", + "email": "marcelina.davis@example.com", + "email_verified": True, + "external_id": "f1ffa2b2-c20b-4d39-be5c-212726e11222", + "metadata": {"timezone": "America/New_York"}, + "last_sign_in_at": "2025-06-25T19:07:33.155Z", + "locale": "en-US", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + } + instance = SendVerificationEmailResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["user"] == data["user"] + + def test_verify_email_response_round_trip(self): + data = load_fixture("verify_email_response.json") + instance = VerifyEmailResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = VerifyEmailResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_verify_email_response_minimal_payload(self): + data = { + "user": { + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": "Marcelina", + "last_name": "Davis", + "profile_picture_url": "https://workoscdn.com/images/v1/123abc", + "email": "marcelina.davis@example.com", + "email_verified": True, + "external_id": "f1ffa2b2-c20b-4d39-be5c-212726e11222", + "metadata": {"timezone": "America/New_York"}, + "last_sign_in_at": "2025-06-25T19:07:33.155Z", + "locale": "en-US", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + } + instance = VerifyEmailResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["user"] == data["user"] + + def test_password_reset_round_trip(self): + data = load_fixture("password_reset.json") + instance = PasswordReset.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = PasswordReset.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_password_reset_minimal_payload(self): + data = { + "object": "password_reset", + "id": "password_reset_01E4ZCR3C56J083X43JQXF3JK5", + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "expires_at": "2026-01-15T12:00:00.000Z", + "created_at": "2026-01-15T12:00:00.000Z", + "password_reset_token": "Z1uX3RbwcIl5fIGJJJCXXisdI", + "password_reset_url": "https://your-app.com/reset-password?token=Z1uX3RbwcIl5fIGJJJCXXisdI", + } + instance = PasswordReset.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["user_id"] == data["user_id"] + assert serialized["email"] == data["email"] + assert serialized["expires_at"] == data["expires_at"] + assert serialized["created_at"] == data["created_at"] + assert serialized["password_reset_token"] == data["password_reset_token"] + assert serialized["password_reset_url"] == data["password_reset_url"] + + def test_reset_password_response_round_trip(self): + data = load_fixture("reset_password_response.json") + instance = ResetPasswordResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = ResetPasswordResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_reset_password_response_minimal_payload(self): + data = { + "user": { + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": "Marcelina", + "last_name": "Davis", + "profile_picture_url": "https://workoscdn.com/images/v1/123abc", + "email": "marcelina.davis@example.com", + "email_verified": True, + "external_id": "f1ffa2b2-c20b-4d39-be5c-212726e11222", + "metadata": {"timezone": "America/New_York"}, + "last_sign_in_at": "2025-06-25T19:07:33.155Z", + "locale": "en-US", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + } + instance = ResetPasswordResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["user"] == data["user"] + + def test_authenticate_response_round_trip(self): + data = load_fixture("authenticate_response.json") + instance = AuthenticateResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuthenticateResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_authenticate_response_minimal_payload(self): + data = { + "user": { + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": "Marcelina", + "last_name": "Davis", + "profile_picture_url": "https://workoscdn.com/images/v1/123abc", + "email": "marcelina.davis@example.com", + "email_verified": True, + "external_id": "f1ffa2b2-c20b-4d39-be5c-212726e11222", + "metadata": {"timezone": "America/New_York"}, + "last_sign_in_at": "2025-06-25T19:07:33.155Z", + "locale": "en-US", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + }, + "access_token": "eyJhb.nNzb19vaWRjX2tleV9.lc5Uk4yWVk5In0", + "refresh_token": "yAjhKk123NLIjdrBdGZPf8pLIDvK", + } + instance = AuthenticateResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["user"] == data["user"] + assert serialized["access_token"] == data["access_token"] + assert serialized["refresh_token"] == data["refresh_token"] + + def test_authenticate_response_omits_absent_optional_non_nullable_fields(self): + data = { + "user": { + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": "Marcelina", + "last_name": "Davis", + "profile_picture_url": "https://workoscdn.com/images/v1/123abc", + "email": "marcelina.davis@example.com", + "email_verified": True, + "external_id": "f1ffa2b2-c20b-4d39-be5c-212726e11222", + "metadata": {"timezone": "America/New_York"}, + "last_sign_in_at": "2025-06-25T19:07:33.155Z", + "locale": "en-US", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + }, + "access_token": "eyJhb.nNzb19vaWRjX2tleV9.lc5Uk4yWVk5In0", + "refresh_token": "yAjhKk123NLIjdrBdGZPf8pLIDvK", + } + instance = AuthenticateResponse.from_dict(data) + serialized = instance.to_dict() + assert "organization_id" not in serialized + assert "authkit_authorization_code" not in serialized + assert "authentication_method" not in serialized + assert "impersonator" not in serialized + assert "oauth_tokens" not in serialized + + def test_authenticate_response_round_trips_unknown_enum_values(self): + data = { + "user": { + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": "Marcelina", + "last_name": "Davis", + "profile_picture_url": "https://workoscdn.com/images/v1/123abc", + "email": "marcelina.davis@example.com", + "email_verified": True, + "external_id": "f1ffa2b2-c20b-4d39-be5c-212726e11222", + "metadata": {"timezone": "America/New_York"}, + "last_sign_in_at": "2025-06-25T19:07:33.155Z", + "locale": "en-US", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + }, + "organization_id": "org_01H945H0YD4F97JN9MATX7BYAG", + "authkit_authorization_code": "authkit_authz_code_abc123", + "access_token": "eyJhb.nNzb19vaWRjX2tleV9.lc5Uk4yWVk5In0", + "refresh_token": "yAjhKk123NLIjdrBdGZPf8pLIDvK", + "authentication_method": "unexpected_authenticate_response_authentication_method", + "impersonator": { + "email": "admin@foocorp.com", + "reason": "Investigating an issue with the customer's account.", + }, + "oauth_tokens": { + "provider": "GoogleOAuth", + "refresh_token": "1//04g...", + "access_token": "ya29.a0ARrdaM...", + "expires_at": 1735141800, + "scopes": ["profile", "email", "openid"], + }, + } + instance = AuthenticateResponse.from_dict(data) + assert instance.to_dict() == data + + def test_device_authorization_response_round_trip(self): + data = load_fixture("device_authorization_response.json") + instance = DeviceAuthorizationResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = DeviceAuthorizationResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_device_authorization_response_minimal_payload(self): + data = { + "device_code": "CVE2wOfIFK4vhmiDBntpX9s8KT2f0qngpWYL0LGy9HxYgBRXUKIUkZB9BgIFho5h", + "user_code": "BCDF-GHJK", + "verification_uri": "https://authkit_domain/device", + "expires_in": 300, + } + instance = DeviceAuthorizationResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["device_code"] == data["device_code"] + assert serialized["user_code"] == data["user_code"] + assert serialized["verification_uri"] == data["verification_uri"] + assert serialized["expires_in"] == data["expires_in"] + + def test_device_authorization_response_omits_absent_optional_non_nullable_fields( + self, + ): + data = { + "device_code": "CVE2wOfIFK4vhmiDBntpX9s8KT2f0qngpWYL0LGy9HxYgBRXUKIUkZB9BgIFho5h", + "user_code": "BCDF-GHJK", + "verification_uri": "https://authkit_domain/device", + "expires_in": 300, + } + instance = DeviceAuthorizationResponse.from_dict(data) + serialized = instance.to_dict() + assert "verification_uri_complete" not in serialized + assert "interval" not in serialized + + def test_webhook_endpoint_json_round_trip(self): + data = load_fixture("webhook_endpoint_json.json") + instance = WebhookEndpointJson.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = WebhookEndpointJson.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_webhook_endpoint_json_minimal_payload(self): + data = { + "object": "webhook_endpoint", + "id": "we_0123456789", + "endpoint_url": "https://example.com/webhooks", + "secret": "whsec_0FWAiVGkEfGBqqsJH4aNAGBJ4", + "status": "enabled", + "events": ["user.created", "dsync.user.created"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = WebhookEndpointJson.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["endpoint_url"] == data["endpoint_url"] + assert serialized["secret"] == data["secret"] + assert serialized["status"] == data["status"] + assert serialized["events"] == data["events"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_webhook_endpoint_json_round_trips_unknown_enum_values(self): + data = { + "object": "webhook_endpoint", + "id": "we_0123456789", + "endpoint_url": "https://example.com/webhooks", + "secret": "whsec_0FWAiVGkEfGBqqsJH4aNAGBJ4", + "status": "unexpected_webhook_endpoint_json_status", + "events": ["user.created", "dsync.user.created"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = WebhookEndpointJson.from_dict(data) + assert instance.to_dict() == data + + def test_widget_session_token_response_round_trip(self): + data = load_fixture("widget_session_token_response.json") + instance = WidgetSessionTokenResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = WidgetSessionTokenResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_widget_session_token_response_minimal_payload(self): + data = {"token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InNlc3Npb24..."} + instance = WidgetSessionTokenResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["token"] == data["token"] + + def test_sso_authorize_url_response_round_trip(self): + data = load_fixture("sso_authorize_url_response.json") + instance = SSOAuthorizeUrlResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = SSOAuthorizeUrlResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_sso_authorize_url_response_minimal_payload(self): + data = { + "url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=example&redirect_uri=https%3A%2F%2Fapi.workos.com%2Fsso%2Fcallback&response_type=code&scope=openid%20profile%20email" + } + instance = SSOAuthorizeUrlResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["url"] == data["url"] + + def test_profile_round_trip(self): + data = load_fixture("profile.json") + instance = Profile.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = Profile.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_profile_minimal_payload(self): + data = { + "object": "profile", + "id": "prof_01DMC79VCBZ0NY2099737PSVF1", + "organization_id": None, + "connection_id": "conn_01E4ZCR3C56J083X43JQXF3JK5", + "connection_type": "GoogleOAuth", + "idp_id": "103456789012345678901", + "email": "todd@example.com", + "first_name": None, + "last_name": None, + "raw_attributes": {"key": {}}, + } + instance = Profile.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["organization_id"] == data["organization_id"] + assert serialized["connection_id"] == data["connection_id"] + assert serialized["connection_type"] == data["connection_type"] + assert serialized["idp_id"] == data["idp_id"] + assert serialized["email"] == data["email"] + assert serialized["first_name"] == data["first_name"] + assert serialized["last_name"] == data["last_name"] + assert serialized["raw_attributes"] == data["raw_attributes"] + + def test_profile_omits_absent_optional_non_nullable_fields(self): + data = { + "object": "profile", + "id": "prof_01DMC79VCBZ0NY2099737PSVF1", + "organization_id": "org_01EHQMYV6MBK39QC5PZXHY59C3", + "connection_id": "conn_01E4ZCR3C56J083X43JQXF3JK5", + "connection_type": "GoogleOAuth", + "idp_id": "103456789012345678901", + "email": "todd@example.com", + "first_name": "Todd", + "last_name": "Rundgren", + "role": {"slug": "admin"}, + "roles": [{"slug": "admin"}], + "raw_attributes": {"key": {}}, + } + instance = Profile.from_dict(data) + serialized = instance.to_dict() + assert "groups" not in serialized + assert "custom_attributes" not in serialized + + def test_profile_preserves_nullable_fields(self): + data = { + "object": "profile", + "id": "prof_01DMC79VCBZ0NY2099737PSVF1", + "organization_id": None, + "connection_id": "conn_01E4ZCR3C56J083X43JQXF3JK5", + "connection_type": "GoogleOAuth", + "idp_id": "103456789012345678901", + "email": "todd@example.com", + "first_name": None, + "last_name": None, + "role": None, + "roles": None, + "groups": ["Engineering", "Admins"], + "custom_attributes": {"key": {}}, + "raw_attributes": {"key": {}}, + } + instance = Profile.from_dict(data) + serialized = instance.to_dict() + assert serialized["organization_id"] is None + assert serialized["first_name"] is None + assert serialized["last_name"] is None + assert serialized["role"] is None + assert serialized["roles"] is None + + def test_profile_round_trips_unknown_enum_values(self): + data = { + "object": "profile", + "id": "prof_01DMC79VCBZ0NY2099737PSVF1", + "organization_id": "org_01EHQMYV6MBK39QC5PZXHY59C3", + "connection_id": "conn_01E4ZCR3C56J083X43JQXF3JK5", + "connection_type": "unexpected_profile_connection_type", + "idp_id": "103456789012345678901", + "email": "todd@example.com", + "first_name": "Todd", + "last_name": "Rundgren", + "role": {"slug": "admin"}, + "roles": [{"slug": "admin"}], + "groups": ["Engineering", "Admins"], + "custom_attributes": {"key": {}}, + "raw_attributes": {"key": {}}, + } + instance = Profile.from_dict(data) + assert instance.to_dict() == data + + def test_sso_token_response_round_trip(self): + data = load_fixture("sso_token_response.json") + instance = SSOTokenResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = SSOTokenResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_sso_token_response_minimal_payload(self): + data = { + "token_type": "Bearer", + "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InNzby...", + "expires_in": 600, + "profile": { + "object": "profile", + "id": "prof_01DMC79VCBZ0NY2099737PSVF1", + "organization_id": "org_01EHQMYV6MBK39QC5PZXHY59C3", + "connection_id": "conn_01E4ZCR3C56J083X43JQXF3JK5", + "connection_type": "GoogleOAuth", + "idp_id": "103456789012345678901", + "email": "todd@example.com", + "first_name": "Todd", + "last_name": "Rundgren", + "role": {"slug": "admin"}, + "roles": [{"slug": "admin"}], + "groups": ["Engineering", "Admins"], + "custom_attributes": {"key": {}}, + "raw_attributes": {"key": {}}, + }, + } + instance = SSOTokenResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["token_type"] == data["token_type"] + assert serialized["access_token"] == data["access_token"] + assert serialized["expires_in"] == data["expires_in"] + assert serialized["profile"] == data["profile"] + + def test_sso_token_response_omits_absent_optional_non_nullable_fields(self): + data = { + "token_type": "Bearer", + "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InNzby...", + "expires_in": 600, + "profile": { + "object": "profile", + "id": "prof_01DMC79VCBZ0NY2099737PSVF1", + "organization_id": "org_01EHQMYV6MBK39QC5PZXHY59C3", + "connection_id": "conn_01E4ZCR3C56J083X43JQXF3JK5", + "connection_type": "GoogleOAuth", + "idp_id": "103456789012345678901", + "email": "todd@example.com", + "first_name": "Todd", + "last_name": "Rundgren", + "role": {"slug": "admin"}, + "roles": [{"slug": "admin"}], + "groups": ["Engineering", "Admins"], + "custom_attributes": {"key": {}}, + "raw_attributes": {"key": {}}, + }, + } + instance = SSOTokenResponse.from_dict(data) + serialized = instance.to_dict() + assert "oauth_tokens" not in serialized + + def test_sso_logout_authorize_response_round_trip(self): + data = load_fixture("sso_logout_authorize_response.json") + instance = SSOLogoutAuthorizeResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = SSOLogoutAuthorizeResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_sso_logout_authorize_response_minimal_payload(self): + data = { + "logout_url": "https://auth.workos.com/sso/logout?token=eyJhbGciOiJSUzI1NiJ9", + "logout_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9maWxlX2lkIjoicHJvZl8wMUdXUTFHMEgyRk02QVNFRjBIUzEzSENXOS0zMDRrZzAzZyIsImV4cCI6IjE1MTYyMzkwMjIifQ.Wru9Qlnf5DpohtGCKhZU4cVOd3zpiu7QQ-XEX--5A_4", + } + instance = SSOLogoutAuthorizeResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["logout_url"] == data["logout_url"] + assert serialized["logout_token"] == data["logout_token"] + + def test_jwks_response_round_trip(self): + data = load_fixture("jwks_response.json") + instance = JwksResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = JwksResponse.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_jwks_response_minimal_payload(self): + data = { + "keys": [ + { + "alg": "RS256", + "kty": "RSA", + "use": "sig", + "x5c": ["MIIDQjCCAiqgAwIBAgIGATz/FuLiMA0GCSqGSIb3DQEBCwUA..."], + "n": "0vx7agoebGc...eKnNs", + "e": "AQAB", + "kid": "key_01HXYZ123456789ABCDEFGHIJ", + "x5t#S256": "ZjQzYjI0OT...NmNjU0", + } + ] + } + instance = JwksResponse.from_dict(data) + serialized = instance.to_dict() + assert serialized["keys"] == data["keys"] + + def test_jwks_response_keys_round_trip(self): + data = load_fixture("jwks_response_keys.json") + instance = JwksResponseKeys.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = JwksResponseKeys.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_jwks_response_keys_minimal_payload(self): + data = { + "alg": "RS256", + "kty": "RSA", + "use": "sig", + "x5c": ["MIIDQjCCAiqgAwIBAgIGATz/FuLiMA0GCSqGSIb3DQEBCwUA..."], + "n": "0vx7agoebGc...eKnNs", + "e": "AQAB", + "kid": "key_01HXYZ123456789ABCDEFGHIJ", + "x5t#S256": "ZjQzYjI0OT...NmNjU0", + } + instance = JwksResponseKeys.from_dict(data) + serialized = instance.to_dict() + assert serialized["alg"] == data["alg"] + assert serialized["kty"] == data["kty"] + assert serialized["use"] == data["use"] + assert serialized["x5c"] == data["x5c"] + assert serialized["n"] == data["n"] + assert serialized["e"] == data["e"] + assert serialized["kid"] == data["kid"] + assert serialized["x5t#S256"] == data["x5t#S256"] + + def test_sso_token_response_oauth_token_round_trip(self): + data = load_fixture("sso_token_response_oauth_token.json") + instance = SSOTokenResponseOAuthToken.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = SSOTokenResponseOAuthToken.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_sso_token_response_oauth_token_minimal_payload(self): + data = { + "provider": "GoogleOAuth", + "refresh_token": "1//04g...", + "access_token": "ya29.a0ARrdaM...", + "expires_at": 1735141800, + "scopes": ["profile", "email", "openid"], + } + instance = SSOTokenResponseOAuthToken.from_dict(data) + serialized = instance.to_dict() + assert serialized["provider"] == data["provider"] + assert serialized["refresh_token"] == data["refresh_token"] + assert serialized["access_token"] == data["access_token"] + assert serialized["expires_at"] == data["expires_at"] + assert serialized["scopes"] == data["scopes"] + + def test_authenticate_response_impersonator_round_trip(self): + data = load_fixture("authenticate_response_impersonator.json") + instance = AuthenticateResponseImpersonator.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuthenticateResponseImpersonator.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_authenticate_response_impersonator_minimal_payload(self): + data = {"email": "admin@foocorp.com", "reason": None} + instance = AuthenticateResponseImpersonator.from_dict(data) + serialized = instance.to_dict() + assert serialized["email"] == data["email"] + assert serialized["reason"] == data["reason"] + + def test_authenticate_response_impersonator_preserves_nullable_fields(self): + data = {"email": "admin@foocorp.com", "reason": None} + instance = AuthenticateResponseImpersonator.from_dict(data) + serialized = instance.to_dict() + assert serialized["reason"] is None + + def test_authenticate_response_oauth_token_round_trip(self): + data = load_fixture("authenticate_response_oauth_token.json") + instance = AuthenticateResponseOAuthToken.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuthenticateResponseOAuthToken.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_authenticate_response_oauth_token_minimal_payload(self): + data = { + "provider": "GoogleOAuth", + "refresh_token": "1//04g...", + "access_token": "ya29.a0ARrdaM...", + "expires_at": 1735141800, + "scopes": ["profile", "email", "openid"], + } + instance = AuthenticateResponseOAuthToken.from_dict(data) + serialized = instance.to_dict() + assert serialized["provider"] == data["provider"] + assert serialized["refresh_token"] == data["refresh_token"] + assert serialized["access_token"] == data["access_token"] + assert serialized["expires_at"] == data["expires_at"] + assert serialized["scopes"] == data["scopes"] + + def test_data_integrations_list_response_data_round_trip(self): + data = load_fixture("data_integrations_list_response_data.json") + instance = DataIntegrationsListResponseData.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = DataIntegrationsListResponseData.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_data_integrations_list_response_data_minimal_payload(self): + data = { + "object": "data_provider", + "id": "data_integration_01EHZNVPK3SFK441A1RGBFSHRT", + "name": "GitHub", + "description": None, + "slug": "github", + "integration_type": "github", + "credentials_type": "oauth2", + "scopes": None, + "ownership": "userland_user", + "created_at": "2024-01-15T10:30:00.000Z", + "updated_at": "2024-01-15T10:30:00.000Z", + "connected_account": None, + } + instance = DataIntegrationsListResponseData.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["name"] == data["name"] + assert serialized["description"] == data["description"] + assert serialized["slug"] == data["slug"] + assert serialized["integration_type"] == data["integration_type"] + assert serialized["credentials_type"] == data["credentials_type"] + assert serialized["scopes"] == data["scopes"] + assert serialized["ownership"] == data["ownership"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + assert serialized["connected_account"] == data["connected_account"] + + def test_data_integrations_list_response_data_preserves_nullable_fields(self): + data = { + "object": "data_provider", + "id": "data_integration_01EHZNVPK3SFK441A1RGBFSHRT", + "name": "GitHub", + "description": None, + "slug": "github", + "integration_type": "github", + "credentials_type": "oauth2", + "scopes": None, + "ownership": "userland_user", + "created_at": "2024-01-15T10:30:00.000Z", + "updated_at": "2024-01-15T10:30:00.000Z", + "connected_account": None, + } + instance = DataIntegrationsListResponseData.from_dict(data) + serialized = instance.to_dict() + assert serialized["description"] is None + assert serialized["scopes"] is None + assert serialized["connected_account"] is None + + def test_data_integrations_list_response_data_round_trips_unknown_enum_values(self): + data = { + "object": "data_provider", + "id": "data_integration_01EHZNVPK3SFK441A1RGBFSHRT", + "name": "GitHub", + "description": "Connect your GitHub account to access repositories.", + "slug": "github", + "integration_type": "github", + "credentials_type": "oauth2", + "scopes": ["repo", "user:email"], + "ownership": "unexpected_data_integrations_list_response_data_ownership", + "created_at": "2024-01-15T10:30:00.000Z", + "updated_at": "2024-01-15T10:30:00.000Z", + "connected_account": { + "object": "connected_account", + "id": "data_installation_01EHZNVPK3SFK441A1RGBFSHRT", + "user_id": "user_01EHZNVPK3SFK441A1RGBFSHRT", + "organization_id": None, + "scopes": ["repo", "user:email"], + "state": "connected", + "created_at": "2024-01-16T14:20:00.000Z", + "updated_at": "2024-01-16T14:20:00.000Z", + "userlandUserId": "test_userlandUserId", + }, + } + instance = DataIntegrationsListResponseData.from_dict(data) + assert instance.to_dict() == data + + def test_audit_log_configuration_log_stream_round_trip(self): + data = load_fixture("audit_log_configuration_log_stream.json") + instance = AuditLogConfigurationLogStream.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuditLogConfigurationLogStream.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_audit_log_configuration_log_stream_minimal_payload(self): + data = { + "id": "als_01EHZNVPK3SFK441A1RGBFSHRT", + "type": "Datadog", + "state": "active", + "last_synced_at": None, + "created_at": "2026-01-15T12:00:00.000Z", + } + instance = AuditLogConfigurationLogStream.from_dict(data) + serialized = instance.to_dict() + assert serialized["id"] == data["id"] + assert serialized["type"] == data["type"] + assert serialized["state"] == data["state"] + assert serialized["last_synced_at"] == data["last_synced_at"] + assert serialized["created_at"] == data["created_at"] + + def test_audit_log_configuration_log_stream_preserves_nullable_fields(self): + data = { + "id": "als_01EHZNVPK3SFK441A1RGBFSHRT", + "type": "Datadog", + "state": "active", + "last_synced_at": None, + "created_at": "2026-01-15T12:00:00.000Z", + } + instance = AuditLogConfigurationLogStream.from_dict(data) + serialized = instance.to_dict() + assert serialized["last_synced_at"] is None + + def test_audit_log_configuration_log_stream_round_trips_unknown_enum_values(self): + data = { + "id": "als_01EHZNVPK3SFK441A1RGBFSHRT", + "type": "unexpected_audit_log_configuration_log_stream_type", + "state": "active", + "last_synced_at": "2026-01-15T12:00:00.000Z", + "created_at": "2026-01-15T12:00:00.000Z", + } + instance = AuditLogConfigurationLogStream.from_dict(data) + assert instance.to_dict() == data + + def test_organization_domain_round_trip(self): + data = load_fixture("organization_domain.json") + instance = OrganizationDomain.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = OrganizationDomain.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_organization_domain_minimal_payload(self): + data = { + "object": "organization_domain", + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "organization_id": "org_01HE8GSH8FQPASKSY27THRKRBP", + "domain": "foo-corp.com", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = OrganizationDomain.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["organization_id"] == data["organization_id"] + assert serialized["domain"] == data["domain"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_organization_domain_omits_absent_optional_non_nullable_fields(self): + data = { + "object": "organization_domain", + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "organization_id": "org_01HE8GSH8FQPASKSY27THRKRBP", + "domain": "foo-corp.com", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = OrganizationDomain.from_dict(data) + serialized = instance.to_dict() + assert "state" not in serialized + assert "verification_prefix" not in serialized + assert "verification_token" not in serialized + assert "verification_strategy" not in serialized + + def test_organization_domain_round_trips_unknown_enum_values(self): + data = { + "object": "organization_domain", + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "organization_id": "org_01HE8GSH8FQPASKSY27THRKRBP", + "domain": "foo-corp.com", + "state": "unexpected_organization_domain_state", + "verification_prefix": "superapp-domain-verification-z3kjny", + "verification_token": "m5Oztg3jdK4NJLgs8uIlIprMw", + "verification_strategy": "dns", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = OrganizationDomain.from_dict(data) + assert instance.to_dict() == data + + def test_api_key_with_value_owner_round_trip(self): + data = load_fixture("api_key_with_value_owner.json") + instance = ApiKeyWithValueOwner.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = ApiKeyWithValueOwner.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_api_key_with_value_owner_minimal_payload(self): + data = {"type": "organization", "id": "org_01EHZNVPK3SFK441A1RGBFSHRT"} + instance = ApiKeyWithValueOwner.from_dict(data) + serialized = instance.to_dict() + assert serialized["type"] == data["type"] + assert serialized["id"] == data["id"] + + def test_flag_owner_round_trip(self): + data = load_fixture("flag_owner.json") + instance = FlagOwner.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = FlagOwner.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_flag_owner_minimal_payload(self): + data = {"email": "jane@example.com", "first_name": None, "last_name": None} + instance = FlagOwner.from_dict(data) + serialized = instance.to_dict() + assert serialized["email"] == data["email"] + assert serialized["first_name"] == data["first_name"] + assert serialized["last_name"] == data["last_name"] + + def test_flag_owner_preserves_nullable_fields(self): + data = {"email": "jane@example.com", "first_name": None, "last_name": None} + instance = FlagOwner.from_dict(data) + serialized = instance.to_dict() + assert serialized["first_name"] is None + assert serialized["last_name"] is None + + def test_directory_user_with_groups_email_round_trip(self): + data = load_fixture("directory_user_with_groups_email.json") + instance = DirectoryUserWithGroupsEmail.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = DirectoryUserWithGroupsEmail.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_directory_user_with_groups_email_minimal_payload(self): + data = {} + instance = DirectoryUserWithGroupsEmail.from_dict(data) + assert instance.to_dict() is not None + + def test_directory_user_with_groups_email_omits_absent_optional_non_nullable_fields( + self, + ): + data = {"value": "marcelina.davis@example.com"} + instance = DirectoryUserWithGroupsEmail.from_dict(data) + serialized = instance.to_dict() + assert "primary" not in serialized + assert "type" not in serialized + + def test_directory_user_with_groups_email_preserves_nullable_fields(self): + data = {"primary": True, "type": "work", "value": None} + instance = DirectoryUserWithGroupsEmail.from_dict(data) + serialized = instance.to_dict() + assert serialized["value"] is None + + def test_directory_metadata_round_trip(self): + data = load_fixture("directory_metadata.json") + instance = DirectoryMetadata.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = DirectoryMetadata.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_directory_metadata_minimal_payload(self): + data = {"users": {"active": 42, "inactive": 3}, "groups": 5} + instance = DirectoryMetadata.from_dict(data) + serialized = instance.to_dict() + assert serialized["users"] == data["users"] + assert serialized["groups"] == data["groups"] + + def test_connection_domain_round_trip(self): + data = load_fixture("connection_domain.json") + instance = ConnectionDomain.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = ConnectionDomain.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_connection_domain_minimal_payload(self): + data = { + "id": "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A", + "object": "connection_domain", + "domain": "foo-corp.com", + } + instance = ConnectionDomain.from_dict(data) + serialized = instance.to_dict() + assert serialized["id"] == data["id"] + assert serialized["object"] == data["object"] + assert serialized["domain"] == data["domain"] + + def test_connection_option_round_trip(self): + data = load_fixture("connection_option.json") + instance = ConnectionOption.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = ConnectionOption.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_connection_option_minimal_payload(self): + data = {"signing_cert": None} + instance = ConnectionOption.from_dict(data) + serialized = instance.to_dict() + assert serialized["signing_cert"] == data["signing_cert"] + + def test_connection_option_preserves_nullable_fields(self): + data = {"signing_cert": None} + instance = ConnectionOption.from_dict(data) + serialized = instance.to_dict() + assert serialized["signing_cert"] is None + + def test_user_organization_membership_base_list_data_round_trip(self): + data = load_fixture("user_organization_membership_base_list_data.json") + instance = UserOrganizationMembershipBaseListData.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = UserOrganizationMembershipBaseListData.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_user_organization_membership_base_list_data_minimal_payload(self): + data = { + "object": "organization_membership", + "id": "om_01HXYZ123456789ABCDEFGHIJ", + "user_id": "user_01EHQTV6MWP9P1F4ZXGXMC8ABB", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "status": "active", + "directory_managed": False, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = UserOrganizationMembershipBaseListData.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["user_id"] == data["user_id"] + assert serialized["organization_id"] == data["organization_id"] + assert serialized["status"] == data["status"] + assert serialized["directory_managed"] == data["directory_managed"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_user_organization_membership_base_list_data_omits_absent_optional_non_nullable_fields( + self, + ): + data = { + "object": "organization_membership", + "id": "om_01HXYZ123456789ABCDEFGHIJ", + "user_id": "user_01EHQTV6MWP9P1F4ZXGXMC8ABB", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "status": "active", + "directory_managed": False, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = UserOrganizationMembershipBaseListData.from_dict(data) + serialized = instance.to_dict() + assert "organization_name" not in serialized + assert "custom_attributes" not in serialized + + def test_user_organization_membership_base_list_data_round_trips_unknown_enum_values( + self, + ): + data = { + "object": "organization_membership", + "id": "om_01HXYZ123456789ABCDEFGHIJ", + "user_id": "user_01EHQTV6MWP9P1F4ZXGXMC8ABB", + "organization_id": "org_01EHZNVPK3SFK441A1RGBFSHRT", + "status": "unexpected_user_organization_membership_base_list_data_status", + "directory_managed": False, + "organization_name": "Acme Corp", + "custom_attributes": { + "department": "Engineering", + "title": "Developer Experience Engineer", + "location": "Brooklyn", + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = UserOrganizationMembershipBaseListData.from_dict(data) + assert instance.to_dict() == data + + def test_role_assignment_resource_round_trip(self): + data = load_fixture("role_assignment_resource.json") + instance = RoleAssignmentResource.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = RoleAssignmentResource.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_role_assignment_resource_minimal_payload(self): + data = { + "id": "authz_resource_01HXYZ123456789ABCDEFGH", + "external_id": "proj-456", + "resource_type_slug": "project", + } + instance = RoleAssignmentResource.from_dict(data) + serialized = instance.to_dict() + assert serialized["id"] == data["id"] + assert serialized["external_id"] == data["external_id"] + assert serialized["resource_type_slug"] == data["resource_type_slug"] + + def test_authentication_factor_sms_round_trip(self): + data = load_fixture("authentication_factor_sms.json") + instance = AuthenticationFactorSms.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuthenticationFactorSms.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_authentication_factor_sms_minimal_payload(self): + data = {"phone_number": "+15005550006"} + instance = AuthenticationFactorSms.from_dict(data) + serialized = instance.to_dict() + assert serialized["phone_number"] == data["phone_number"] + + def test_authentication_factor_totp_round_trip(self): + data = load_fixture("authentication_factor_totp.json") + instance = AuthenticationFactorTotp.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuthenticationFactorTotp.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_authentication_factor_totp_minimal_payload(self): + data = {"issuer": "WorkOS", "user": "user@example.com"} + instance = AuthenticationFactorTotp.from_dict(data) + serialized = instance.to_dict() + assert serialized["issuer"] == data["issuer"] + assert serialized["user"] == data["user"] + + def test_authentication_factor_enrolled_sms_round_trip(self): + data = load_fixture("authentication_factor_enrolled_sms.json") + instance = AuthenticationFactorEnrolledSms.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuthenticationFactorEnrolledSms.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_authentication_factor_enrolled_sms_minimal_payload(self): + data = {"phone_number": "+15005550006"} + instance = AuthenticationFactorEnrolledSms.from_dict(data) + serialized = instance.to_dict() + assert serialized["phone_number"] == data["phone_number"] + + def test_authentication_factor_enrolled_totp_round_trip(self): + data = load_fixture("authentication_factor_enrolled_totp.json") + instance = AuthenticationFactorEnrolledTotp.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuthenticationFactorEnrolledTotp.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_authentication_factor_enrolled_totp_minimal_payload(self): + data = { + "issuer": "WorkOS", + "user": "user@example.com", + "secret": "JBSWY3DPEHPK3PXP", + "qr_code": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...", + "uri": "otpauth://totp/WorkOS:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=WorkOS", + } + instance = AuthenticationFactorEnrolledTotp.from_dict(data) + serialized = instance.to_dict() + assert serialized["issuer"] == data["issuer"] + assert serialized["user"] == data["user"] + assert serialized["secret"] == data["secret"] + assert serialized["qr_code"] == data["qr_code"] + assert serialized["uri"] == data["uri"] + + def test_audit_log_schema_json_actor_round_trip(self): + data = load_fixture("audit_log_schema_json_actor.json") + instance = AuditLogSchemaJsonActor.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuditLogSchemaJsonActor.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_audit_log_schema_json_actor_minimal_payload(self): + data = {"metadata": {"key": {}}} + instance = AuditLogSchemaJsonActor.from_dict(data) + serialized = instance.to_dict() + assert serialized["metadata"] == data["metadata"] + + def test_audit_log_schema_json_target_round_trip(self): + data = load_fixture("audit_log_schema_json_target.json") + instance = AuditLogSchemaJsonTarget.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuditLogSchemaJsonTarget.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_audit_log_schema_json_target_minimal_payload(self): + data = {"type": "invoice"} + instance = AuditLogSchemaJsonTarget.from_dict(data) + serialized = instance.to_dict() + assert serialized["type"] == data["type"] + + def test_audit_log_schema_json_target_omits_absent_optional_non_nullable_fields( + self, + ): + data = {"type": "invoice"} + instance = AuditLogSchemaJsonTarget.from_dict(data) + serialized = instance.to_dict() + assert "metadata" not in serialized + + def test_authorized_connect_application_list_data_round_trip(self): + data = load_fixture("authorized_connect_application_list_data.json") + instance = AuthorizedConnectApplicationListData.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = AuthorizedConnectApplicationListData.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_authorized_connect_application_list_data_minimal_payload(self): + data = { + "object": "authorized_connect_application", + "id": "aca_01HXYZ123456789ABCDEFGHIJ", + "granted_scopes": ["openid", "profile", "email"], + "application": { + "object": "connect_application", + "id": "conn_app_01HXYZ123456789ABCDEFGHIJ", + "client_id": "client_01HXYZ123456789ABCDEFGHIJ", + "description": "An application for managing user access", + "name": "My Application", + "scopes": ["openid", "profile", "email"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + }, + } + instance = AuthorizedConnectApplicationListData.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["granted_scopes"] == data["granted_scopes"] + assert serialized["application"] == data["application"] + + def test_api_key_owner_round_trip(self): + data = load_fixture("api_key_owner.json") + instance = ApiKeyOwner.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = ApiKeyOwner.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_api_key_owner_minimal_payload(self): + data = {"type": "organization", "id": "org_01EHZNVPK3SFK441A1RGBFSHRT"} + instance = ApiKeyOwner.from_dict(data) + serialized = instance.to_dict() + assert serialized["type"] == data["type"] + assert serialized["id"] == data["id"] + + def test_user_consent_option_choice_round_trip(self): + data = load_fixture("user_consent_option_choice.json") + instance = UserConsentOptionChoice.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = UserConsentOptionChoice.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_user_consent_option_choice_minimal_payload(self): + data = {} + instance = UserConsentOptionChoice.from_dict(data) + assert instance.to_dict() is not None + + def test_user_consent_option_choice_omits_absent_optional_non_nullable_fields(self): + data = {} + instance = UserConsentOptionChoice.from_dict(data) + serialized = instance.to_dict() + assert "value" not in serialized + assert "label" not in serialized + + def test_list_round_trip(self): + data = load_fixture("list.json") + instance = ListModel.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = ListModel.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_list_minimal_payload(self): + data = { + "object": "list", + "data": [ + { + "slug": "org-admin", + "object": "role", + "id": "role_01EHQMYV6MBK39QC5PZXHY59C3", + "name": "Organization Admin", + "description": "Can manage all resources", + "type": "OrganizationRole", + "resource_type_slug": "default", + "permissions": ["posts:read", "posts:write"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + ], + } + instance = ListModel.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["data"] == data["data"] + + def test_list_data_round_trip(self): + data = load_fixture("list_data.json") + instance = ListData.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = ListData.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_list_data_minimal_payload(self): + data = { + "slug": "org-admin", + "object": "role", + "id": "role_01EHQMYV6MBK39QC5PZXHY59C3", + "name": "Organization Admin", + "description": None, + "type": "OrganizationRole", + "resource_type_slug": "default", + "permissions": ["posts:read", "posts:write"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = ListData.from_dict(data) + serialized = instance.to_dict() + assert serialized["slug"] == data["slug"] + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["name"] == data["name"] + assert serialized["description"] == data["description"] + assert serialized["type"] == data["type"] + assert serialized["resource_type_slug"] == data["resource_type_slug"] + assert serialized["permissions"] == data["permissions"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_list_data_preserves_nullable_fields(self): + data = { + "slug": "org-admin", + "object": "role", + "id": "role_01EHQMYV6MBK39QC5PZXHY59C3", + "name": "Organization Admin", + "description": None, + "type": "OrganizationRole", + "resource_type_slug": "default", + "permissions": ["posts:read", "posts:write"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = ListData.from_dict(data) + serialized = instance.to_dict() + assert serialized["description"] is None + + def test_list_data_round_trips_unknown_enum_values(self): + data = { + "slug": "org-admin", + "object": "role", + "id": "role_01EHQMYV6MBK39QC5PZXHY59C3", + "name": "Organization Admin", + "description": "Can manage all resources", + "type": "unexpected_list_data_type", + "resource_type_slug": "default", + "permissions": ["posts:read", "posts:write"], + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = ListData.from_dict(data) + assert instance.to_dict() == data + + def test_permission_round_trip(self): + data = load_fixture("permission.json") + instance = Permission.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = Permission.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_permission_minimal_payload(self): + data = { + "object": "permission", + "id": "perm_01HXYZ123456789ABCDEFGHIJ", + "slug": "documents:read", + "name": "View Documents", + "description": None, + "system": False, + "resource_type_slug": "document", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = Permission.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["slug"] == data["slug"] + assert serialized["name"] == data["name"] + assert serialized["description"] == data["description"] + assert serialized["system"] == data["system"] + assert serialized["resource_type_slug"] == data["resource_type_slug"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_permission_preserves_nullable_fields(self): + data = { + "object": "permission", + "id": "perm_01HXYZ123456789ABCDEFGHIJ", + "slug": "documents:read", + "name": "View Documents", + "description": None, + "system": False, + "resource_type_slug": "document", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = Permission.from_dict(data) + serialized = instance.to_dict() + assert serialized["description"] is None + + def test_application_credentials_list_item_round_trip(self): + data = load_fixture("application_credentials_list_item.json") + instance = ApplicationCredentialsListItem.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = ApplicationCredentialsListItem.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_application_credentials_list_item_minimal_payload(self): + data = { + "object": "connect_application_secret", + "id": "secret_01J9Q2Z3X4Y5W6V7U8T9S0R1Q", + "secret_hint": "abc123", + "last_used_at": None, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = ApplicationCredentialsListItem.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["secret_hint"] == data["secret_hint"] + assert serialized["last_used_at"] == data["last_used_at"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_application_credentials_list_item_preserves_nullable_fields(self): + data = { + "object": "connect_application_secret", + "id": "secret_01J9Q2Z3X4Y5W6V7U8T9S0R1Q", + "secret_hint": "abc123", + "last_used_at": None, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = ApplicationCredentialsListItem.from_dict(data) + serialized = instance.to_dict() + assert serialized["last_used_at"] is None + + def test_feature_flag_round_trip(self): + data = load_fixture("feature_flag.json") + instance = FeatureFlag.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = FeatureFlag.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_feature_flag_minimal_payload(self): + data = { + "object": "feature_flag", + "id": "flag_01EHZNVPK3SFK441A1RGBFSHRT", + "slug": "advanced-analytics", + "name": "Advanced Analytics", + "description": None, + "owner": None, + "tags": ["reports"], + "enabled": False, + "default_value": False, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = FeatureFlag.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["slug"] == data["slug"] + assert serialized["name"] == data["name"] + assert serialized["description"] == data["description"] + assert serialized["owner"] == data["owner"] + assert serialized["tags"] == data["tags"] + assert serialized["enabled"] == data["enabled"] + assert serialized["default_value"] == data["default_value"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_feature_flag_preserves_nullable_fields(self): + data = { + "object": "feature_flag", + "id": "flag_01EHZNVPK3SFK441A1RGBFSHRT", + "slug": "advanced-analytics", + "name": "Advanced Analytics", + "description": None, + "owner": None, + "tags": ["reports"], + "enabled": False, + "default_value": False, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = FeatureFlag.from_dict(data) + serialized = instance.to_dict() + assert serialized["description"] is None + assert serialized["owner"] is None + + def test_feature_flag_owner_round_trip(self): + data = load_fixture("feature_flag_owner.json") + instance = FeatureFlagOwner.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = FeatureFlagOwner.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_feature_flag_owner_minimal_payload(self): + data = {"email": "jane@example.com", "first_name": None, "last_name": None} + instance = FeatureFlagOwner.from_dict(data) + serialized = instance.to_dict() + assert serialized["email"] == data["email"] + assert serialized["first_name"] == data["first_name"] + assert serialized["last_name"] == data["last_name"] + + def test_feature_flag_owner_preserves_nullable_fields(self): + data = {"email": "jane@example.com", "first_name": None, "last_name": None} + instance = FeatureFlagOwner.from_dict(data) + serialized = instance.to_dict() + assert serialized["first_name"] is None + assert serialized["last_name"] is None + + def test_invitation_round_trip(self): + data = load_fixture("invitation.json") + instance = Invitation.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = Invitation.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_invitation_minimal_payload(self): + data = { + "object": "invitation", + "id": "invitation_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "state": "accepted", + "accepted_at": None, + "revoked_at": None, + "expires_at": "2026-01-15T12:00:00.000Z", + "organization_id": None, + "inviter_user_id": None, + "accepted_user_id": None, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "token": "Z1uX3RbwcIl5fIGJJJCXXisdI", + "accept_invitation_url": "https://your-app.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI", + } + instance = Invitation.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["email"] == data["email"] + assert serialized["state"] == data["state"] + assert serialized["accepted_at"] == data["accepted_at"] + assert serialized["revoked_at"] == data["revoked_at"] + assert serialized["expires_at"] == data["expires_at"] + assert serialized["organization_id"] == data["organization_id"] + assert serialized["inviter_user_id"] == data["inviter_user_id"] + assert serialized["accepted_user_id"] == data["accepted_user_id"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + assert serialized["token"] == data["token"] + assert serialized["accept_invitation_url"] == data["accept_invitation_url"] + + def test_invitation_preserves_nullable_fields(self): + data = { + "object": "invitation", + "id": "invitation_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "state": "accepted", + "accepted_at": None, + "revoked_at": None, + "expires_at": "2026-01-15T12:00:00.000Z", + "organization_id": None, + "inviter_user_id": None, + "accepted_user_id": None, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "token": "Z1uX3RbwcIl5fIGJJJCXXisdI", + "accept_invitation_url": "https://your-app.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI", + } + instance = Invitation.from_dict(data) + serialized = instance.to_dict() + assert serialized["accepted_at"] is None + assert serialized["revoked_at"] is None + assert serialized["organization_id"] is None + assert serialized["inviter_user_id"] is None + assert serialized["accepted_user_id"] is None + + def test_invitation_round_trips_unknown_enum_values(self): + data = { + "object": "invitation", + "id": "invitation_01E4ZCR3C56J083X43JQXF3JK5", + "email": "marcelina.davis@example.com", + "state": "unexpected_invitation_state", + "accepted_at": "2026-01-15T12:00:00.000Z", + "revoked_at": None, + "expires_at": "2026-01-15T12:00:00.000Z", + "organization_id": "org_01E4ZCR3C56J083X43JQXF3JK5", + "inviter_user_id": "user_01HYGBX8ZGD19949T3BM4FW1C3", + "accepted_user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "token": "Z1uX3RbwcIl5fIGJJJCXXisdI", + "accept_invitation_url": "https://your-app.com/invite?invitation_token=Z1uX3RbwcIl5fIGJJJCXXisdI", + } + instance = Invitation.from_dict(data) + assert instance.to_dict() == data + + def test_organization_membership_round_trip(self): + data = load_fixture("organization_membership.json") + instance = OrganizationMembership.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = OrganizationMembership.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_organization_membership_minimal_payload(self): + data = { + "object": "organization_membership", + "id": "om_01HXYZ123456789ABCDEFGHIJ", + "user_id": "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", + "organization_id": "org_01E4ZCR3C56J083X43JQXF3JK5", + "status": "active", + "directory_managed": False, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "role": {"slug": "admin"}, + } + instance = OrganizationMembership.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["user_id"] == data["user_id"] + assert serialized["organization_id"] == data["organization_id"] + assert serialized["status"] == data["status"] + assert serialized["directory_managed"] == data["directory_managed"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + assert serialized["role"] == data["role"] + + def test_organization_membership_omits_absent_optional_non_nullable_fields(self): + data = { + "object": "organization_membership", + "id": "om_01HXYZ123456789ABCDEFGHIJ", + "user_id": "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", + "organization_id": "org_01E4ZCR3C56J083X43JQXF3JK5", + "status": "active", + "directory_managed": False, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "role": {"slug": "admin"}, + } + instance = OrganizationMembership.from_dict(data) + serialized = instance.to_dict() + assert "organization_name" not in serialized + assert "custom_attributes" not in serialized + + def test_organization_membership_round_trips_unknown_enum_values(self): + data = { + "object": "organization_membership", + "id": "om_01HXYZ123456789ABCDEFGHIJ", + "user_id": "user_01E4ZCR3C5A4QZ2Z2JQXGKZJ9E", + "organization_id": "org_01E4ZCR3C56J083X43JQXF3JK5", + "status": "unexpected_organization_membership_status", + "directory_managed": False, + "organization_name": "Acme Corp", + "custom_attributes": { + "department": "Engineering", + "title": "Developer Experience Engineer", + "location": "Brooklyn", + }, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + "role": {"slug": "admin"}, + } + instance = OrganizationMembership.from_dict(data) + assert instance.to_dict() == data + + def test_user_identities_get_item_round_trip(self): + data = load_fixture("user_identities_get_item.json") + instance = UserIdentitiesGetItem.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = UserIdentitiesGetItem.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_user_identities_get_item_minimal_payload(self): + data = { + "idp_id": "4F42ABDE-1E44-4B66-824A-5F733C037A6D", + "type": "OAuth", + "provider": "MicrosoftOAuth", + } + instance = UserIdentitiesGetItem.from_dict(data) + serialized = instance.to_dict() + assert serialized["idp_id"] == data["idp_id"] + assert serialized["type"] == data["type"] + assert serialized["provider"] == data["provider"] + + def test_user_identities_get_item_round_trips_unknown_enum_values(self): + data = { + "idp_id": "4F42ABDE-1E44-4B66-824A-5F733C037A6D", + "type": "OAuth", + "provider": "unexpected_user_identities_get_item_provider", + } + instance = UserIdentitiesGetItem.from_dict(data) + assert instance.to_dict() == data + + def test_user_sessions_list_item_round_trip(self): + data = load_fixture("user_sessions_list_item.json") + instance = UserSessionsListItem.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = UserSessionsListItem.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_user_sessions_list_item_minimal_payload(self): + data = { + "object": "session", + "id": "session_01H93ZY4F80QPBEZ1R5B2SHQG8", + "ip_address": None, + "user_agent": None, + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "auth_method": "sso", + "status": "active", + "expires_at": "2026-01-15T12:00:00.000Z", + "ended_at": None, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = UserSessionsListItem.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["ip_address"] == data["ip_address"] + assert serialized["user_agent"] == data["user_agent"] + assert serialized["user_id"] == data["user_id"] + assert serialized["auth_method"] == data["auth_method"] + assert serialized["status"] == data["status"] + assert serialized["expires_at"] == data["expires_at"] + assert serialized["ended_at"] == data["ended_at"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_user_sessions_list_item_omits_absent_optional_non_nullable_fields(self): + data = { + "object": "session", + "id": "session_01H93ZY4F80QPBEZ1R5B2SHQG8", + "ip_address": "198.51.100.42", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "auth_method": "sso", + "status": "active", + "expires_at": "2026-01-15T12:00:00.000Z", + "ended_at": None, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = UserSessionsListItem.from_dict(data) + serialized = instance.to_dict() + assert "impersonator" not in serialized + assert "organization_id" not in serialized + + def test_user_sessions_list_item_preserves_nullable_fields(self): + data = { + "object": "session", + "id": "session_01H93ZY4F80QPBEZ1R5B2SHQG8", + "impersonator": { + "email": "admin@foocorp.com", + "reason": "Investigating an issue with the customer's account.", + }, + "ip_address": None, + "organization_id": "org_01H945H0YD4F97JN9MATX7BYAG", + "user_agent": None, + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "auth_method": "sso", + "status": "active", + "expires_at": "2026-01-15T12:00:00.000Z", + "ended_at": None, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = UserSessionsListItem.from_dict(data) + serialized = instance.to_dict() + assert serialized["ip_address"] is None + assert serialized["user_agent"] is None + assert serialized["ended_at"] is None + + def test_user_sessions_list_item_round_trips_unknown_enum_values(self): + data = { + "object": "session", + "id": "session_01H93ZY4F80QPBEZ1R5B2SHQG8", + "impersonator": { + "email": "admin@foocorp.com", + "reason": "Investigating an issue with the customer's account.", + }, + "ip_address": "198.51.100.42", + "organization_id": "org_01H945H0YD4F97JN9MATX7BYAG", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36", + "user_id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "auth_method": "unexpected_user_sessions_list_item_auth_method", + "status": "active", + "expires_at": "2026-01-15T12:00:00.000Z", + "ended_at": None, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = UserSessionsListItem.from_dict(data) + assert instance.to_dict() == data + + def test_user_sessions_impersonator_round_trip(self): + data = load_fixture("user_sessions_impersonator.json") + instance = UserSessionsImpersonator.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = UserSessionsImpersonator.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_user_sessions_impersonator_minimal_payload(self): + data = {"email": "admin@foocorp.com", "reason": None} + instance = UserSessionsImpersonator.from_dict(data) + serialized = instance.to_dict() + assert serialized["email"] == data["email"] + assert serialized["reason"] == data["reason"] + + def test_user_sessions_impersonator_preserves_nullable_fields(self): + data = {"email": "admin@foocorp.com", "reason": None} + instance = UserSessionsImpersonator.from_dict(data) + serialized = instance.to_dict() + assert serialized["reason"] is None + + def test_directory_metadata_user_round_trip(self): + data = load_fixture("directory_metadata_user.json") + instance = DirectoryMetadataUser.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = DirectoryMetadataUser.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_directory_metadata_user_minimal_payload(self): + data = {"active": 42, "inactive": 3} + instance = DirectoryMetadataUser.from_dict(data) + serialized = instance.to_dict() + assert serialized["active"] == data["active"] + assert serialized["inactive"] == data["inactive"] + + def test_data_integrations_list_response_data_connected_account_round_trip(self): + data = load_fixture( + "data_integrations_list_response_data_connected_account.json" + ) + instance = DataIntegrationsListResponseDataConnectedAccount.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = DataIntegrationsListResponseDataConnectedAccount.from_dict( + serialized + ) + assert restored.to_dict() == serialized + + def test_data_integrations_list_response_data_connected_account_minimal_payload( + self, + ): + data = { + "object": "connected_account", + "id": "data_installation_01EHZNVPK3SFK441A1RGBFSHRT", + "user_id": None, + "organization_id": None, + "scopes": ["repo", "user:email"], + "state": "connected", + "created_at": "2024-01-16T14:20:00.000Z", + "updated_at": "2024-01-16T14:20:00.000Z", + "userlandUserId": None, + } + instance = DataIntegrationsListResponseDataConnectedAccount.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["user_id"] == data["user_id"] + assert serialized["organization_id"] == data["organization_id"] + assert serialized["scopes"] == data["scopes"] + assert serialized["state"] == data["state"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + assert serialized["userlandUserId"] == data["userlandUserId"] + + def test_data_integrations_list_response_data_connected_account_preserves_nullable_fields( + self, + ): + data = { + "object": "connected_account", + "id": "data_installation_01EHZNVPK3SFK441A1RGBFSHRT", + "user_id": None, + "organization_id": None, + "scopes": ["repo", "user:email"], + "state": "connected", + "created_at": "2024-01-16T14:20:00.000Z", + "updated_at": "2024-01-16T14:20:00.000Z", + "userlandUserId": None, + } + instance = DataIntegrationsListResponseDataConnectedAccount.from_dict(data) + serialized = instance.to_dict() + assert serialized["user_id"] is None + assert serialized["organization_id"] is None + assert serialized["userlandUserId"] is None + + def test_data_integrations_list_response_data_connected_account_round_trips_unknown_enum_values( + self, + ): + data = { + "object": "connected_account", + "id": "data_installation_01EHZNVPK3SFK441A1RGBFSHRT", + "user_id": "user_01EHZNVPK3SFK441A1RGBFSHRT", + "organization_id": None, + "scopes": ["repo", "user:email"], + "state": "unexpected_data_integrations_list_response_data_connected_account_state", + "created_at": "2024-01-16T14:20:00.000Z", + "updated_at": "2024-01-16T14:20:00.000Z", + "userlandUserId": "test_userlandUserId", + } + instance = DataIntegrationsListResponseDataConnectedAccount.from_dict(data) + assert instance.to_dict() == data diff --git a/tests/test_multi_factor_auth.py b/tests/test_multi_factor_auth.py new file mode 100644 index 00000000..5576d526 --- /dev/null +++ b/tests/test_multi_factor_auth.py @@ -0,0 +1,212 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.common.models import AuthenticationFactorsCreateRequestType +from workos.multi_factor_auth.models import ( + AuthenticationFactor, + AuthenticationFactorEnrolled, +) +from workos.multi_factor_auth.challenges.models import AuthenticationChallenge +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestMultiFactorAuth: + def test_create(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("authentication_factor_enrolled.json"), + ) + result = workos.multi_factor_auth.create( + type=AuthenticationFactorsCreateRequestType("generic_otp") + ) + assert isinstance(result, AuthenticationFactorEnrolled) + assert result.object == "authentication_factor" + assert result.id == "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/auth/factors/enroll") + body = json.loads(request.content) + assert body["type"] == AuthenticationFactorsCreateRequestType("generic_otp") + + def test_get(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("authentication_factor.json"), + ) + result = workos.multi_factor_auth.get("test_id") + assert isinstance(result, AuthenticationFactor) + assert result.object == "authentication_factor" + assert result.id == "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/auth/factors/test_id") + + def test_delete(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.multi_factor_auth.delete("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/auth/factors/test_id") + + def test_challenge(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("authentication_challenge.json"), + ) + result = workos.multi_factor_auth.challenge("test_id") + assert isinstance(result, AuthenticationChallenge) + assert result.object == "authentication_challenge" + assert result.id == "auth_challenge_01FVYZ5QM8N98T9ME5BCB2BBMJ" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/auth/factors/test_id/challenge") + + def test_create_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.multi_factor_auth.create( + type=AuthenticationFactorsCreateRequestType("generic_otp") + ) + + def test_create_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.multi_factor_auth.create( + type=AuthenticationFactorsCreateRequestType("generic_otp") + ) + finally: + workos.close() + + def test_create_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.multi_factor_auth.create( + type=AuthenticationFactorsCreateRequestType("generic_otp") + ) + finally: + workos.close() + + def test_create_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.multi_factor_auth.create( + type=AuthenticationFactorsCreateRequestType("generic_otp") + ) + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncMultiFactorAuth: + async def test_create(self, async_workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("authentication_factor_enrolled.json") + ) + result = await async_workos.multi_factor_auth.create( + type=AuthenticationFactorsCreateRequestType("generic_otp") + ) + assert isinstance(result, AuthenticationFactorEnrolled) + assert result.object == "authentication_factor" + assert result.id == "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/auth/factors/enroll") + + async def test_get(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("authentication_factor.json")) + result = await async_workos.multi_factor_auth.get("test_id") + assert isinstance(result, AuthenticationFactor) + assert result.object == "authentication_factor" + assert result.id == "auth_factor_01FVYZ5QM8N98T9ME5BCB2BBMJ" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/auth/factors/test_id") + + async def test_delete(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.multi_factor_auth.delete("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/auth/factors/test_id") + + async def test_challenge(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("authentication_challenge.json")) + result = await async_workos.multi_factor_auth.challenge("test_id") + assert isinstance(result, AuthenticationChallenge) + assert result.object == "authentication_challenge" + assert result.id == "auth_challenge_01FVYZ5QM8N98T9ME5BCB2BBMJ" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/auth/factors/test_id/challenge") + + async def test_create_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.multi_factor_auth.create( + type=AuthenticationFactorsCreateRequestType("generic_otp") + ) + + async def test_create_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.multi_factor_auth.create( + type=AuthenticationFactorsCreateRequestType("generic_otp") + ) + finally: + await workos.close() + + async def test_create_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.multi_factor_auth.create( + type=AuthenticationFactorsCreateRequestType("generic_otp") + ) + finally: + await workos.close() + + async def test_create_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.multi_factor_auth.create( + type=AuthenticationFactorsCreateRequestType("generic_otp") + ) + finally: + await workos.close() diff --git a/tests/test_multi_factor_auth_challenges.py b/tests/test_multi_factor_auth_challenges.py new file mode 100644 index 00000000..95e60549 --- /dev/null +++ b/tests/test_multi_factor_auth_challenges.py @@ -0,0 +1,137 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.multi_factor_auth.challenges.models import ( + AuthenticationChallengeVerifyResponse, +) +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestMultiFactorAuthChallenges: + def test_verify(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("authentication_challenge_verify_response.json"), + ) + result = workos.multi_factor_auth.challenges.verify("test_id", code="test_code") + assert isinstance(result, AuthenticationChallengeVerifyResponse) + assert result.valid is True + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/auth/challenges/test_id/verify") + body = json.loads(request.content) + assert body["code"] == "test_code" + + def test_verify_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.multi_factor_auth.challenges.verify("test_id", code="test_code") + + def test_verify_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.multi_factor_auth.challenges.verify("test_id", code="test_code") + finally: + workos.close() + + def test_verify_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.multi_factor_auth.challenges.verify("test_id", code="test_code") + finally: + workos.close() + + def test_verify_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.multi_factor_auth.challenges.verify("test_id", code="test_code") + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncMultiFactorAuthChallenges: + async def test_verify(self, async_workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("authentication_challenge_verify_response.json") + ) + result = await async_workos.multi_factor_auth.challenges.verify( + "test_id", code="test_code" + ) + assert isinstance(result, AuthenticationChallengeVerifyResponse) + assert result.valid is True + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/auth/challenges/test_id/verify") + + async def test_verify_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.multi_factor_auth.challenges.verify( + "test_id", code="test_code" + ) + + async def test_verify_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.multi_factor_auth.challenges.verify( + "test_id", code="test_code" + ) + finally: + await workos.close() + + async def test_verify_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.multi_factor_auth.challenges.verify( + "test_id", code="test_code" + ) + finally: + await workos.close() + + async def test_verify_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.multi_factor_auth.challenges.verify( + "test_id", code="test_code" + ) + finally: + await workos.close() diff --git a/tests/test_organization_domains.py b/tests/test_organization_domains.py new file mode 100644 index 00000000..5556e063 --- /dev/null +++ b/tests/test_organization_domains.py @@ -0,0 +1,221 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.organization_domains.models import ( + OrganizationDomain, + OrganizationDomainStandAlone, +) +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestOrganizationDomains: + def test_create_organization_domain(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("organization_domain.json"), + ) + result = workos.organization_domains.create_organization_domain( + domain="test_domain", organization_id="test_organization_id" + ) + assert isinstance(result, OrganizationDomain) + assert result.object == "organization_domain" + assert result.id == "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/organization_domains") + body = json.loads(request.content) + assert body["domain"] == "test_domain" + assert body["organization_id"] == "test_organization_id" + + def test_get_organization_domain(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("organization_domain_stand_alone.json"), + ) + result = workos.organization_domains.get_organization_domain("test_id") + assert isinstance(result, OrganizationDomainStandAlone) + assert result.object == "organization_domain" + assert result.id == "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/organization_domains/test_id") + + def test_delete_organization_domain(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.organization_domains.delete_organization_domain("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/organization_domains/test_id") + + def test_verify_organization_domain(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("organization_domain_stand_alone.json"), + ) + result = workos.organization_domains.verify_organization_domain("test_id") + assert isinstance(result, OrganizationDomainStandAlone) + assert result.object == "organization_domain" + assert result.id == "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/organization_domains/test_id/verify") + + def test_create_organization_domain_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.organization_domains.create_organization_domain( + domain="test_domain", organization_id="test_organization_id" + ) + + def test_create_organization_domain_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.organization_domains.create_organization_domain( + domain="test_domain", organization_id="test_organization_id" + ) + finally: + workos.close() + + def test_create_organization_domain_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.organization_domains.create_organization_domain( + domain="test_domain", organization_id="test_organization_id" + ) + finally: + workos.close() + + def test_create_organization_domain_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.organization_domains.create_organization_domain( + domain="test_domain", organization_id="test_organization_id" + ) + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncOrganizationDomains: + async def test_create_organization_domain(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("organization_domain.json")) + result = await async_workos.organization_domains.create_organization_domain( + domain="test_domain", organization_id="test_organization_id" + ) + assert isinstance(result, OrganizationDomain) + assert result.object == "organization_domain" + assert result.id == "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/organization_domains") + + async def test_get_organization_domain(self, async_workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("organization_domain_stand_alone.json") + ) + result = await async_workos.organization_domains.get_organization_domain( + "test_id" + ) + assert isinstance(result, OrganizationDomainStandAlone) + assert result.object == "organization_domain" + assert result.id == "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/organization_domains/test_id") + + async def test_delete_organization_domain(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.organization_domains.delete_organization_domain( + "test_id" + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/organization_domains/test_id") + + async def test_verify_organization_domain(self, async_workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("organization_domain_stand_alone.json") + ) + result = await async_workos.organization_domains.verify_organization_domain( + "test_id" + ) + assert isinstance(result, OrganizationDomainStandAlone) + assert result.object == "organization_domain" + assert result.id == "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/organization_domains/test_id/verify") + + async def test_create_organization_domain_unauthorized( + self, async_workos, httpx_mock + ): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.organization_domains.create_organization_domain( + domain="test_domain", organization_id="test_organization_id" + ) + + async def test_create_organization_domain_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.organization_domains.create_organization_domain( + domain="test_domain", organization_id="test_organization_id" + ) + finally: + await workos.close() + + async def test_create_organization_domain_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.organization_domains.create_organization_domain( + domain="test_domain", organization_id="test_organization_id" + ) + finally: + await workos.close() + + async def test_create_organization_domain_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.organization_domains.create_organization_domain( + domain="test_domain", organization_id="test_organization_id" + ) + finally: + await workos.close() diff --git a/tests/test_organizations.py b/tests/test_organizations.py new file mode 100644 index 00000000..9f59d24c --- /dev/null +++ b/tests/test_organizations.py @@ -0,0 +1,343 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.organizations.models import ( + AuditLogConfiguration, + AuditLogsRetentionJson, + Organization, + OrganizationsOrder, +) +from workos._pagination import AsyncPage, SyncPage +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestOrganizations: + def test_list(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_organization.json"), + ) + page = workos.organizations.list() + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.organizations.list() + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.organizations.list( + limit=10, + before="cursor before", + after="cursor/after", + order=OrganizationsOrder("normal"), + search="value search/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["search"] == "value search/test" + + def test_create(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("organization.json"), + ) + result = workos.organizations.create(name="test_name") + assert isinstance(result, Organization) + assert result.object == "organization" + assert result.id == "org_01EHWNCE74X7JSDV0X3SZ3KJNY" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/organizations") + body = json.loads(request.content) + assert body["name"] == "test_name" + + def test_get_by_external_id(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("organization.json"), + ) + result = workos.organizations.get_by_external_id("test_external_id") + assert isinstance(result, Organization) + assert result.object == "organization" + assert result.id == "org_01EHWNCE74X7JSDV0X3SZ3KJNY" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/organizations/external_id/test_external_id") + + def test_get(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("organization.json"), + ) + result = workos.organizations.get("test_id") + assert isinstance(result, Organization) + assert result.object == "organization" + assert result.id == "org_01EHWNCE74X7JSDV0X3SZ3KJNY" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/organizations/test_id") + + def test_update_organization(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("organization.json"), + ) + result = workos.organizations.update("test_id") + assert isinstance(result, Organization) + assert result.object == "organization" + assert result.id == "org_01EHWNCE74X7JSDV0X3SZ3KJNY" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/organizations/test_id") + + def test_delete_organization(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.organizations.delete("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/organizations/test_id") + + def test_get_audit_log_configuration(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("audit_log_configuration.json"), + ) + result = workos.organizations.get_audit_log_configuration("test_id") + assert isinstance(result, AuditLogConfiguration) + assert result.organization_id == "org_01EHZNVPK3SFK441A1RGBFSHRT" + assert result.retention_period_in_days == 30 + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith( + "/organizations/test_id/audit_log_configuration" + ) + + def test_audit_logs_retention(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("audit_logs_retention_json.json"), + ) + result = workos.organizations.audit_logs_retention("test_id") + assert isinstance(result, AuditLogsRetentionJson) + assert result.retention_period_in_days == 30 + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/organizations/test_id/audit_logs_retention") + + def test_update_audit_logs_retention(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("audit_logs_retention_json.json"), + ) + result = workos.organizations.update_audit_logs_retention( + "test_id", retention_period_in_days=1 + ) + assert isinstance(result, AuditLogsRetentionJson) + assert result.retention_period_in_days == 30 + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/organizations/test_id/audit_logs_retention") + body = json.loads(request.content) + assert body["retention_period_in_days"] == 1 + + def test_list_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.organizations.list() + + def test_list_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.organizations.list() + finally: + workos.close() + + def test_list_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.organizations.list() + finally: + workos.close() + + def test_list_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.organizations.list() + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncOrganizations: + async def test_list(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_organization.json")) + page = await async_workos.organizations.list() + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.organizations.list() + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.organizations.list( + limit=10, + before="cursor before", + after="cursor/after", + order=OrganizationsOrder("normal"), + search="value search/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["search"] == "value search/test" + + async def test_create(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("organization.json")) + result = await async_workos.organizations.create(name="test_name") + assert isinstance(result, Organization) + assert result.object == "organization" + assert result.id == "org_01EHWNCE74X7JSDV0X3SZ3KJNY" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/organizations") + + async def test_get_by_external_id(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("organization.json")) + result = await async_workos.organizations.get_by_external_id("test_external_id") + assert isinstance(result, Organization) + assert result.object == "organization" + assert result.id == "org_01EHWNCE74X7JSDV0X3SZ3KJNY" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/organizations/external_id/test_external_id") + + async def test_get(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("organization.json")) + result = await async_workos.organizations.get("test_id") + assert isinstance(result, Organization) + assert result.object == "organization" + assert result.id == "org_01EHWNCE74X7JSDV0X3SZ3KJNY" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/organizations/test_id") + + async def test_update_organization(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("organization.json")) + result = await async_workos.organizations.update("test_id") + assert isinstance(result, Organization) + assert result.object == "organization" + assert result.id == "org_01EHWNCE74X7JSDV0X3SZ3KJNY" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/organizations/test_id") + + async def test_delete_organization(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.organizations.delete("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/organizations/test_id") + + async def test_get_audit_log_configuration(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("audit_log_configuration.json")) + result = await async_workos.organizations.get_audit_log_configuration("test_id") + assert isinstance(result, AuditLogConfiguration) + assert result.organization_id == "org_01EHZNVPK3SFK441A1RGBFSHRT" + assert result.retention_period_in_days == 30 + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith( + "/organizations/test_id/audit_log_configuration" + ) + + async def test_audit_logs_retention(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("audit_logs_retention_json.json")) + result = await async_workos.organizations.audit_logs_retention("test_id") + assert isinstance(result, AuditLogsRetentionJson) + assert result.retention_period_in_days == 30 + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/organizations/test_id/audit_logs_retention") + + async def test_update_audit_logs_retention(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("audit_logs_retention_json.json")) + result = await async_workos.organizations.update_audit_logs_retention( + "test_id", retention_period_in_days=1 + ) + assert isinstance(result, AuditLogsRetentionJson) + assert result.retention_period_in_days == 30 + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/organizations/test_id/audit_logs_retention") + + async def test_list_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.organizations.list() + + async def test_list_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.organizations.list() + finally: + await workos.close() + + async def test_list_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.organizations.list() + finally: + await workos.close() + + async def test_list_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.organizations.list() + finally: + await workos.close() diff --git a/tests/test_organizations_api_keys.py b/tests/test_organizations_api_keys.py new file mode 100644 index 00000000..808772fc --- /dev/null +++ b/tests/test_organizations_api_keys.py @@ -0,0 +1,189 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.organizations.api_keys.models import ( + ApiKeyWithValue, + OrganizationsApiKeysOrder, +) +from workos._pagination import AsyncPage, SyncPage +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestOrganizationsApiKeys: + def test_list(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_api_key.json"), + ) + page = workos.organizations.api_keys.list("test_organizationId") + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.organizations.api_keys.list("test_organizationId") + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.organizations.api_keys.list( + "test_organizationId", + limit=10, + before="cursor before", + after="cursor/after", + order=OrganizationsApiKeysOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + def test_create(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("api_key_with_value.json"), + ) + result = workos.organizations.api_keys.create( + "test_organizationId", name="test_name" + ) + assert isinstance(result, ApiKeyWithValue) + assert result.object == "api_key" + assert result.id == "api_key_01EHZNVPK3SFK441A1RGBFSHRT" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/organizations/test_organizationId/api_keys") + body = json.loads(request.content) + assert body["name"] == "test_name" + + def test_list_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.organizations.api_keys.list("test_organizationId") + + def test_list_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.organizations.api_keys.list("test_organizationId") + finally: + workos.close() + + def test_list_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.organizations.api_keys.list("test_organizationId") + finally: + workos.close() + + def test_list_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.organizations.api_keys.list("test_organizationId") + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncOrganizationsApiKeys: + async def test_list(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_api_key.json")) + page = await async_workos.organizations.api_keys.list("test_organizationId") + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.organizations.api_keys.list("test_organizationId") + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.organizations.api_keys.list( + "test_organizationId", + limit=10, + before="cursor before", + after="cursor/after", + order=OrganizationsApiKeysOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + async def test_create(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("api_key_with_value.json")) + result = await async_workos.organizations.api_keys.create( + "test_organizationId", name="test_name" + ) + assert isinstance(result, ApiKeyWithValue) + assert result.object == "api_key" + assert result.id == "api_key_01EHZNVPK3SFK441A1RGBFSHRT" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/organizations/test_organizationId/api_keys") + + async def test_list_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.organizations.api_keys.list("test_organizationId") + + async def test_list_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.organizations.api_keys.list("test_organizationId") + finally: + await workos.close() + + async def test_list_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.organizations.api_keys.list("test_organizationId") + finally: + await workos.close() + + async def test_list_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.organizations.api_keys.list("test_organizationId") + finally: + await workos.close() diff --git a/tests/test_organizations_feature_flags.py b/tests/test_organizations_feature_flags.py new file mode 100644 index 00000000..cc199980 --- /dev/null +++ b/tests/test_organizations_feature_flags.py @@ -0,0 +1,161 @@ +# This file is auto-generated by oagen. Do not edit. + + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.organizations.feature_flags.models import OrganizationsFeatureFlagsOrder +from workos._pagination import AsyncPage, SyncPage +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestOrganizationsFeatureFlags: + def test_list(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_flag.json"), + ) + page = workos.organizations.feature_flags.list("test_organizationId") + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.organizations.feature_flags.list("test_organizationId") + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.organizations.feature_flags.list( + "test_organizationId", + limit=10, + before="cursor before", + after="cursor/after", + order=OrganizationsFeatureFlagsOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + def test_list_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.organizations.feature_flags.list("test_organizationId") + + def test_list_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.organizations.feature_flags.list("test_organizationId") + finally: + workos.close() + + def test_list_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.organizations.feature_flags.list("test_organizationId") + finally: + workos.close() + + def test_list_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.organizations.feature_flags.list("test_organizationId") + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncOrganizationsFeatureFlags: + async def test_list(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_flag.json")) + page = await async_workos.organizations.feature_flags.list( + "test_organizationId" + ) + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.organizations.feature_flags.list( + "test_organizationId" + ) + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.organizations.feature_flags.list( + "test_organizationId", + limit=10, + before="cursor before", + after="cursor/after", + order=OrganizationsFeatureFlagsOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + async def test_list_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.organizations.feature_flags.list("test_organizationId") + + async def test_list_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.organizations.feature_flags.list("test_organizationId") + finally: + await workos.close() + + async def test_list_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.organizations.feature_flags.list("test_organizationId") + finally: + await workos.close() + + async def test_list_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.organizations.feature_flags.list("test_organizationId") + finally: + await workos.close() diff --git a/tests/test_pagination.py b/tests/test_pagination.py new file mode 100644 index 00000000..d5f97dc7 --- /dev/null +++ b/tests/test_pagination.py @@ -0,0 +1,108 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Pagination tests: auto_paging_iter, before cursor stripping.""" + +import pytest + +from workos._pagination import SyncPage, AsyncPage +from dataclasses import dataclass +from typing import Any, Dict + + +@dataclass +class FakeItem: + id: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "FakeItem": + return cls(id=data["id"]) + + def to_dict(self) -> Dict[str, Any]: + return {"id": self.id} + + +class TestSyncPage: + def test_has_more_with_after_cursor(self): + page = SyncPage( + data=[FakeItem(id="1")], + list_metadata={"after": "cursor_abc"}, + ) + assert page.has_more() is True + assert page.after == "cursor_abc" + + def test_has_more_without_cursor(self): + page = SyncPage( + data=[FakeItem(id="1")], + list_metadata={}, + ) + assert page.has_more() is False + + def test_auto_paging_iter_single_page(self): + page = SyncPage( + data=[FakeItem(id="1"), FakeItem(id="2")], + list_metadata={}, + ) + items = list(page.auto_paging_iter()) + assert len(items) == 2 + assert items[0].id == "1" + assert items[1].id == "2" + + def test_auto_paging_iter_multi_page(self): + page2 = SyncPage( + data=[FakeItem(id="3")], + list_metadata={}, + ) + page1 = SyncPage( + data=[FakeItem(id="1"), FakeItem(id="2")], + list_metadata={"after": "cursor_abc"}, + _fetch_page=lambda after=None: page2, + ) + items = list(page1.auto_paging_iter()) + assert len(items) == 3 + assert [i.id for i in items] == ["1", "2", "3"] + + +@pytest.mark.asyncio +class TestAsyncPage: + async def test_has_more_with_after_cursor(self): + page = AsyncPage( + data=[FakeItem(id="1")], + list_metadata={"after": "cursor_abc"}, + ) + assert page.has_more() is True + assert page.after == "cursor_abc" + + async def test_has_more_without_cursor(self): + page = AsyncPage( + data=[FakeItem(id="1")], + list_metadata={}, + ) + assert page.has_more() is False + + async def test_auto_paging_iter_single_page(self): + page = AsyncPage( + data=[FakeItem(id="1"), FakeItem(id="2")], + list_metadata={}, + ) + items = [item async for item in page.auto_paging_iter()] + assert len(items) == 2 + assert items[0].id == "1" + assert items[1].id == "2" + + async def test_auto_paging_iter_multi_page(self): + page2 = AsyncPage( + data=[FakeItem(id="3")], + list_metadata={}, + ) + + async def _fetch(after=None): + return page2 + + page1 = AsyncPage( + data=[FakeItem(id="1"), FakeItem(id="2")], + list_metadata={"after": "cursor_abc"}, + _fetch_page=_fetch, + ) + items = [item async for item in page1.auto_paging_iter()] + assert len(items) == 3 + assert [i.id for i in items] == ["1", "2", "3"] diff --git a/tests/test_permissions.py b/tests/test_permissions.py new file mode 100644 index 00000000..d646760a --- /dev/null +++ b/tests/test_permissions.py @@ -0,0 +1,247 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.permissions.models import ( + AuthorizationPermission, + Permission, + PermissionsOrder, +) +from workos._pagination import AsyncPage, SyncPage +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestPermissions: + def test_list(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_authorization_permission.json"), + ) + page = workos.permissions.list() + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.permissions.list() + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.permissions.list( + limit=10, + before="cursor before", + after="cursor/after", + order=PermissionsOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + def test_create(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("permission.json"), + ) + result = workos.permissions.create(slug="test_slug", name="test_name") + assert isinstance(result, Permission) + assert result.object == "permission" + assert result.id == "perm_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/authorization/permissions") + body = json.loads(request.content) + assert body["slug"] == "test_slug" + assert body["name"] == "test_name" + + def test_get(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("authorization_permission.json"), + ) + result = workos.permissions.get("test_slug") + assert isinstance(result, AuthorizationPermission) + assert result.object == "permission" + assert result.id == "perm_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/authorization/permissions/test_slug") + + def test_update(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("authorization_permission.json"), + ) + result = workos.permissions.update("test_slug") + assert isinstance(result, AuthorizationPermission) + assert result.object == "permission" + assert result.id == "perm_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "PATCH" + assert request.url.path.endswith("/authorization/permissions/test_slug") + + def test_delete(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.permissions.delete("test_slug") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/authorization/permissions/test_slug") + + def test_list_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.permissions.list() + + def test_list_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.permissions.list() + finally: + workos.close() + + def test_list_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.permissions.list() + finally: + workos.close() + + def test_list_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.permissions.list() + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncPermissions: + async def test_list(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_authorization_permission.json")) + page = await async_workos.permissions.list() + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.permissions.list() + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.permissions.list( + limit=10, + before="cursor before", + after="cursor/after", + order=PermissionsOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + async def test_create(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("permission.json")) + result = await async_workos.permissions.create( + slug="test_slug", name="test_name" + ) + assert isinstance(result, Permission) + assert result.object == "permission" + assert result.id == "perm_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/authorization/permissions") + + async def test_get(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("authorization_permission.json")) + result = await async_workos.permissions.get("test_slug") + assert isinstance(result, AuthorizationPermission) + assert result.object == "permission" + assert result.id == "perm_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/authorization/permissions/test_slug") + + async def test_update(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("authorization_permission.json")) + result = await async_workos.permissions.update("test_slug") + assert isinstance(result, AuthorizationPermission) + assert result.object == "permission" + assert result.id == "perm_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "PATCH" + assert request.url.path.endswith("/authorization/permissions/test_slug") + + async def test_delete(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.permissions.delete("test_slug") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/authorization/permissions/test_slug") + + async def test_list_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.permissions.list() + + async def test_list_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.permissions.list() + finally: + await workos.close() + + async def test_list_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.permissions.list() + finally: + await workos.close() + + async def test_list_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.permissions.list() + finally: + await workos.close() diff --git a/tests/test_pipes.py b/tests/test_pipes.py new file mode 100644 index 00000000..954d245d --- /dev/null +++ b/tests/test_pipes.py @@ -0,0 +1,182 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.pipes.models import ( + DataIntegrationAccessTokenResponse, + DataIntegrationAuthorizeUrlResponse, +) +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestPipes: + def test_get_data_integration_authorize_url(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("data_integration_authorize_url_response.json"), + ) + result = workos.pipes.get_data_integration_authorize_url( + "test_slug", user_id="test_user_id" + ) + assert isinstance(result, DataIntegrationAuthorizeUrlResponse) + assert ( + result.url + == "https://api.workos.com/data-integrations/q2czJKmVAraSBg8xFpT7M9uR/authorize-redirect" + ) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/data-integrations/test_slug/authorize") + body = json.loads(request.content) + assert body["user_id"] == "test_user_id" + + def test_get_userland_user_token(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("data_integration_access_token_response.json"), + ) + result = workos.pipes.get_userland_user_token( + "test_slug", user_id="test_user_id" + ) + assert isinstance(result, DataIntegrationAccessTokenResponse) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/data-integrations/test_slug/token") + body = json.loads(request.content) + assert body["user_id"] == "test_user_id" + + def test_get_data_integration_authorize_url_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.pipes.get_data_integration_authorize_url( + "test_slug", user_id="test_user_id" + ) + + def test_get_data_integration_authorize_url_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.pipes.get_data_integration_authorize_url( + "test_slug", user_id="test_user_id" + ) + finally: + workos.close() + + def test_get_data_integration_authorize_url_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.pipes.get_data_integration_authorize_url( + "test_slug", user_id="test_user_id" + ) + finally: + workos.close() + + def test_get_data_integration_authorize_url_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.pipes.get_data_integration_authorize_url( + "test_slug", user_id="test_user_id" + ) + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncPipes: + async def test_get_data_integration_authorize_url(self, async_workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("data_integration_authorize_url_response.json") + ) + result = await async_workos.pipes.get_data_integration_authorize_url( + "test_slug", user_id="test_user_id" + ) + assert isinstance(result, DataIntegrationAuthorizeUrlResponse) + assert ( + result.url + == "https://api.workos.com/data-integrations/q2czJKmVAraSBg8xFpT7M9uR/authorize-redirect" + ) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/data-integrations/test_slug/authorize") + + async def test_get_userland_user_token(self, async_workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("data_integration_access_token_response.json") + ) + result = await async_workos.pipes.get_userland_user_token( + "test_slug", user_id="test_user_id" + ) + assert isinstance(result, DataIntegrationAccessTokenResponse) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/data-integrations/test_slug/token") + + async def test_get_data_integration_authorize_url_unauthorized( + self, async_workos, httpx_mock + ): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.pipes.get_data_integration_authorize_url( + "test_slug", user_id="test_user_id" + ) + + async def test_get_data_integration_authorize_url_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.pipes.get_data_integration_authorize_url( + "test_slug", user_id="test_user_id" + ) + finally: + await workos.close() + + async def test_get_data_integration_authorize_url_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.pipes.get_data_integration_authorize_url( + "test_slug", user_id="test_user_id" + ) + finally: + await workos.close() + + async def test_get_data_integration_authorize_url_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.pipes.get_data_integration_authorize_url( + "test_slug", user_id="test_user_id" + ) + finally: + await workos.close() diff --git a/tests/test_radar.py b/tests/test_radar.py new file mode 100644 index 00000000..e04e05ef --- /dev/null +++ b/tests/test_radar.py @@ -0,0 +1,258 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.common.models import ( + RadarStandaloneAssessRequestAction, + RadarStandaloneAssessRequestAuthMethod, +) +from workos.radar.models import ( + RadarListEntryAlreadyPresentResponse, + RadarStandaloneResponse, +) +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestRadar: + def test_assess(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("radar_standalone_response.json"), + ) + result = workos.radar.assess( + ip_address="test_ip_address", + user_agent="test_user_agent", + email="test_email", + auth_method=RadarStandaloneAssessRequestAuthMethod("Password"), + action=RadarStandaloneAssessRequestAction("login"), + ) + assert isinstance(result, RadarStandaloneResponse) + assert result.verdict == "block" + assert result.reason == "Detected enabled Radar control" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/radar/attempts") + body = json.loads(request.content) + assert body["ip_address"] == "test_ip_address" + assert body["user_agent"] == "test_user_agent" + assert body["email"] == "test_email" + assert body["auth_method"] == RadarStandaloneAssessRequestAuthMethod("Password") + assert body["action"] == RadarStandaloneAssessRequestAction("login") + + def test_update_radar_attempt(self, workos, httpx_mock): + httpx_mock.add_response(json={}) + workos.radar.update_radar_attempt("test_id") + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/radar/attempts/test_id") + + def test_update_radar_list(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("radar_list_entry_already_present_response.json"), + ) + result = workos.radar.update_radar_list( + "test_type", "test_action", entry="test_entry" + ) + assert isinstance(result, RadarListEntryAlreadyPresentResponse) + assert result.message == "Entry already present in list" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/radar/lists/test_type/test_action") + body = json.loads(request.content) + assert body["entry"] == "test_entry" + + def test_delete_radar_list_entry(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.radar.delete_radar_list_entry( + "test_type", "test_action", entry="test_entry" + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/radar/lists/test_type/test_action") + + def test_assess_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.radar.assess( + ip_address="test_ip_address", + user_agent="test_user_agent", + email="test_email", + auth_method=RadarStandaloneAssessRequestAuthMethod("Password"), + action=RadarStandaloneAssessRequestAction("login"), + ) + + def test_assess_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.radar.assess( + ip_address="test_ip_address", + user_agent="test_user_agent", + email="test_email", + auth_method=RadarStandaloneAssessRequestAuthMethod("Password"), + action=RadarStandaloneAssessRequestAction("login"), + ) + finally: + workos.close() + + def test_assess_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.radar.assess( + ip_address="test_ip_address", + user_agent="test_user_agent", + email="test_email", + auth_method=RadarStandaloneAssessRequestAuthMethod("Password"), + action=RadarStandaloneAssessRequestAction("login"), + ) + finally: + workos.close() + + def test_assess_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.radar.assess( + ip_address="test_ip_address", + user_agent="test_user_agent", + email="test_email", + auth_method=RadarStandaloneAssessRequestAuthMethod("Password"), + action=RadarStandaloneAssessRequestAction("login"), + ) + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncRadar: + async def test_assess(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("radar_standalone_response.json")) + result = await async_workos.radar.assess( + ip_address="test_ip_address", + user_agent="test_user_agent", + email="test_email", + auth_method=RadarStandaloneAssessRequestAuthMethod("Password"), + action=RadarStandaloneAssessRequestAction("login"), + ) + assert isinstance(result, RadarStandaloneResponse) + assert result.verdict == "block" + assert result.reason == "Detected enabled Radar control" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/radar/attempts") + + async def test_update_radar_attempt(self, async_workos, httpx_mock): + httpx_mock.add_response(json={}) + await async_workos.radar.update_radar_attempt("test_id") + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/radar/attempts/test_id") + + async def test_update_radar_list(self, async_workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("radar_list_entry_already_present_response.json") + ) + result = await async_workos.radar.update_radar_list( + "test_type", "test_action", entry="test_entry" + ) + assert isinstance(result, RadarListEntryAlreadyPresentResponse) + assert result.message == "Entry already present in list" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/radar/lists/test_type/test_action") + + async def test_delete_radar_list_entry(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.radar.delete_radar_list_entry( + "test_type", "test_action", entry="test_entry" + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/radar/lists/test_type/test_action") + + async def test_assess_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.radar.assess( + ip_address="test_ip_address", + user_agent="test_user_agent", + email="test_email", + auth_method=RadarStandaloneAssessRequestAuthMethod("Password"), + action=RadarStandaloneAssessRequestAction("login"), + ) + + async def test_assess_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.radar.assess( + ip_address="test_ip_address", + user_agent="test_user_agent", + email="test_email", + auth_method=RadarStandaloneAssessRequestAuthMethod("Password"), + action=RadarStandaloneAssessRequestAction("login"), + ) + finally: + await workos.close() + + async def test_assess_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.radar.assess( + ip_address="test_ip_address", + user_agent="test_user_agent", + email="test_email", + auth_method=RadarStandaloneAssessRequestAuthMethod("Password"), + action=RadarStandaloneAssessRequestAction("login"), + ) + finally: + await workos.close() + + async def test_assess_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.radar.assess( + ip_address="test_ip_address", + user_agent="test_user_agent", + email="test_email", + auth_method=RadarStandaloneAssessRequestAuthMethod("Password"), + action=RadarStandaloneAssessRequestAction("login"), + ) + finally: + await workos.close() diff --git a/tests/test_sso.py b/tests/test_sso.py new file mode 100644 index 00000000..b01ff00c --- /dev/null +++ b/tests/test_sso.py @@ -0,0 +1,224 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.sso.models import Profile, SSOLogoutAuthorizeResponse, SSOTokenResponse +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestSSO: + def test_authorize(self, workos): + result = workos.sso.authorize( + client_id="test_client_id", + redirect_uri="test_redirect_uri", + response_type="code", + ) + assert isinstance(result, str) + assert result.startswith("http") + + def test_logout(self, workos): + result = workos.sso.logout(token="test_token") + assert isinstance(result, str) + assert result.startswith("http") + + def test_logout_authorize(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("sso_logout_authorize_response.json"), + ) + result = workos.sso.logout_authorize(profile_id="test_profile_id") + assert isinstance(result, SSOLogoutAuthorizeResponse) + assert ( + result.logout_url + == "https://auth.workos.com/sso/logout?token=eyJhbGciOiJSUzI1NiJ9" + ) + assert ( + result.logout_token + == "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9maWxlX2lkIjoicHJvZl8wMUdXUTFHMEgyRk02QVNFRjBIUzEzSENXOS0zMDRrZzAzZyIsImV4cCI6IjE1MTYyMzkwMjIifQ.Wru9Qlnf5DpohtGCKhZU4cVOd3zpiu7QQ-XEX--5A_4" + ) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/sso/logout/authorize") + body = json.loads(request.content) + assert body["profile_id"] == "test_profile_id" + + def test_get_profile(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("profile.json"), + ) + result = workos.sso.get_profile(access_token="test_access_token") + assert isinstance(result, Profile) + assert result.object == "profile" + assert result.id == "prof_01DMC79VCBZ0NY2099737PSVF1" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/sso/profile") + + def test_get_profile_and_token(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("sso_token_response.json"), + ) + result = workos.sso.get_profile_and_token( + client_id="test_client_id", + client_secret="test_client_secret", + code="test_code", + grant_type="authorization_code", + ) + assert isinstance(result, SSOTokenResponse) + assert result.token_type == "Bearer" + assert result.access_token == "eyJhbGciOiJSUzI1NiIsImtpZCI6InNzby..." + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/sso/token") + body = json.loads(request.content) + assert body["client_id"] == "test_client_id" + assert body["client_secret"] == "test_client_secret" + assert body["code"] == "test_code" + assert body["grant_type"] == "authorization_code" + + def test_logout_authorize_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.sso.logout_authorize(profile_id="test_profile_id") + + def test_logout_authorize_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.sso.logout_authorize(profile_id="test_profile_id") + finally: + workos.close() + + def test_logout_authorize_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.sso.logout_authorize(profile_id="test_profile_id") + finally: + workos.close() + + def test_logout_authorize_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.sso.logout_authorize(profile_id="test_profile_id") + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncSSO: + async def test_authorize(self, async_workos): + result = await async_workos.sso.authorize( + client_id="test_client_id", + redirect_uri="test_redirect_uri", + response_type="code", + ) + assert isinstance(result, str) + assert result.startswith("http") + + async def test_logout(self, async_workos): + result = await async_workos.sso.logout(token="test_token") + assert isinstance(result, str) + assert result.startswith("http") + + async def test_logout_authorize(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("sso_logout_authorize_response.json")) + result = await async_workos.sso.logout_authorize(profile_id="test_profile_id") + assert isinstance(result, SSOLogoutAuthorizeResponse) + assert ( + result.logout_url + == "https://auth.workos.com/sso/logout?token=eyJhbGciOiJSUzI1NiJ9" + ) + assert ( + result.logout_token + == "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9maWxlX2lkIjoicHJvZl8wMUdXUTFHMEgyRk02QVNFRjBIUzEzSENXOS0zMDRrZzAzZyIsImV4cCI6IjE1MTYyMzkwMjIifQ.Wru9Qlnf5DpohtGCKhZU4cVOd3zpiu7QQ-XEX--5A_4" + ) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/sso/logout/authorize") + + async def test_get_profile(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("profile.json")) + result = await async_workos.sso.get_profile(access_token="test_access_token") + assert isinstance(result, Profile) + assert result.object == "profile" + assert result.id == "prof_01DMC79VCBZ0NY2099737PSVF1" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/sso/profile") + + async def test_get_profile_and_token(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("sso_token_response.json")) + result = await async_workos.sso.get_profile_and_token( + client_id="test_client_id", + client_secret="test_client_secret", + code="test_code", + grant_type="authorization_code", + ) + assert isinstance(result, SSOTokenResponse) + assert result.token_type == "Bearer" + assert result.access_token == "eyJhbGciOiJSUzI1NiIsImtpZCI6InNzby..." + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/sso/token") + + async def test_logout_authorize_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.sso.logout_authorize(profile_id="test_profile_id") + + async def test_logout_authorize_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.sso.logout_authorize(profile_id="test_profile_id") + finally: + await workos.close() + + async def test_logout_authorize_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.sso.logout_authorize(profile_id="test_profile_id") + finally: + await workos.close() + + async def test_logout_authorize_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.sso.logout_authorize(profile_id="test_profile_id") + finally: + await workos.close() diff --git a/tests/test_user_management_authentication.py b/tests/test_user_management_authentication.py new file mode 100644 index 00000000..62d8b8e1 --- /dev/null +++ b/tests/test_user_management_authentication.py @@ -0,0 +1,246 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.user_management.authentication.models import ( + AuthenticateResponse, + DeviceAuthorizationResponse, +) +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestUserManagementAuthentication: + def test_authenticate(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("authenticate_response.json"), + ) + result = workos.user_management.authentication.authenticate( + body=load_fixture("authorization_code_session_authenticate_request.json") + ) + assert isinstance(result, AuthenticateResponse) + assert result.organization_id == "org_01H945H0YD4F97JN9MATX7BYAG" + assert result.authkit_authorization_code == "authkit_authz_code_abc123" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/authenticate") + + def test_authorize(self, workos): + result = workos.user_management.authentication.authorize( + response_type="code", + redirect_uri="test_redirect_uri", + client_id="test_client_id", + ) + assert isinstance(result, str) + assert result.startswith("http") + + def test_device_authorization(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("device_authorization_response.json"), + ) + result = workos.user_management.authentication.device_authorization( + client_id="test_client_id" + ) + assert isinstance(result, DeviceAuthorizationResponse) + assert ( + result.device_code + == "CVE2wOfIFK4vhmiDBntpX9s8KT2f0qngpWYL0LGy9HxYgBRXUKIUkZB9BgIFho5h" + ) + assert result.user_code == "BCDF-GHJK" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/authorize/device") + body = json.loads(request.content) + assert body["client_id"] == "test_client_id" + + def test_logout(self, workos): + result = workos.user_management.authentication.logout( + session_id="test_session_id" + ) + assert isinstance(result, str) + assert result.startswith("http") + + def test_revoke_session(self, workos, httpx_mock): + httpx_mock.add_response(json={}) + workos.user_management.authentication.revoke_session( + session_id="test_session_id" + ) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/sessions/revoke") + + def test_authenticate_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.user_management.authentication.authenticate( + body=load_fixture( + "authorization_code_session_authenticate_request.json" + ) + ) + + def test_authenticate_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.user_management.authentication.authenticate( + body=load_fixture( + "authorization_code_session_authenticate_request.json" + ) + ) + finally: + workos.close() + + def test_authenticate_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.user_management.authentication.authenticate( + body=load_fixture( + "authorization_code_session_authenticate_request.json" + ) + ) + finally: + workos.close() + + def test_authenticate_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.user_management.authentication.authenticate( + body=load_fixture( + "authorization_code_session_authenticate_request.json" + ) + ) + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncUserManagementAuthentication: + async def test_authenticate(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("authenticate_response.json")) + result = await async_workos.user_management.authentication.authenticate( + body=load_fixture("authorization_code_session_authenticate_request.json") + ) + assert isinstance(result, AuthenticateResponse) + assert result.organization_id == "org_01H945H0YD4F97JN9MATX7BYAG" + assert result.authkit_authorization_code == "authkit_authz_code_abc123" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/authenticate") + + async def test_authorize(self, async_workos): + result = await async_workos.user_management.authentication.authorize( + response_type="code", + redirect_uri="test_redirect_uri", + client_id="test_client_id", + ) + assert isinstance(result, str) + assert result.startswith("http") + + async def test_device_authorization(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("device_authorization_response.json")) + result = await async_workos.user_management.authentication.device_authorization( + client_id="test_client_id" + ) + assert isinstance(result, DeviceAuthorizationResponse) + assert ( + result.device_code + == "CVE2wOfIFK4vhmiDBntpX9s8KT2f0qngpWYL0LGy9HxYgBRXUKIUkZB9BgIFho5h" + ) + assert result.user_code == "BCDF-GHJK" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/authorize/device") + + async def test_logout(self, async_workos): + result = await async_workos.user_management.authentication.logout( + session_id="test_session_id" + ) + assert isinstance(result, str) + assert result.startswith("http") + + async def test_revoke_session(self, async_workos, httpx_mock): + httpx_mock.add_response(json={}) + await async_workos.user_management.authentication.revoke_session( + session_id="test_session_id" + ) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/sessions/revoke") + + async def test_authenticate_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.user_management.authentication.authenticate( + body=load_fixture( + "authorization_code_session_authenticate_request.json" + ) + ) + + async def test_authenticate_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.user_management.authentication.authenticate( + body=load_fixture( + "authorization_code_session_authenticate_request.json" + ) + ) + finally: + await workos.close() + + async def test_authenticate_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.user_management.authentication.authenticate( + body=load_fixture( + "authorization_code_session_authenticate_request.json" + ) + ) + finally: + await workos.close() + + async def test_authenticate_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.user_management.authentication.authenticate( + body=load_fixture( + "authorization_code_session_authenticate_request.json" + ) + ) + finally: + await workos.close() diff --git a/tests/test_user_management_cors_origins.py b/tests/test_user_management_cors_origins.py new file mode 100644 index 00000000..7ffd8429 --- /dev/null +++ b/tests/test_user_management_cors_origins.py @@ -0,0 +1,143 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.user_management.cors_origins.models import CORSOriginResponse +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestUserManagementCorsOrigins: + def test_create_cors_origin(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("cors_origin_response.json"), + ) + result = workos.user_management.cors_origins.create_cors_origin( + origin="test_origin" + ) + assert isinstance(result, CORSOriginResponse) + assert result.object == "cors_origin" + assert result.id == "cors_origin_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/cors_origins") + body = json.loads(request.content) + assert body["origin"] == "test_origin" + + def test_create_cors_origin_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.user_management.cors_origins.create_cors_origin(origin="test_origin") + + def test_create_cors_origin_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.user_management.cors_origins.create_cors_origin( + origin="test_origin" + ) + finally: + workos.close() + + def test_create_cors_origin_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.user_management.cors_origins.create_cors_origin( + origin="test_origin" + ) + finally: + workos.close() + + def test_create_cors_origin_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.user_management.cors_origins.create_cors_origin( + origin="test_origin" + ) + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncUserManagementCorsOrigins: + async def test_create_cors_origin(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("cors_origin_response.json")) + result = await async_workos.user_management.cors_origins.create_cors_origin( + origin="test_origin" + ) + assert isinstance(result, CORSOriginResponse) + assert result.object == "cors_origin" + assert result.id == "cors_origin_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/cors_origins") + + async def test_create_cors_origin_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.user_management.cors_origins.create_cors_origin( + origin="test_origin" + ) + + async def test_create_cors_origin_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.user_management.cors_origins.create_cors_origin( + origin="test_origin" + ) + finally: + await workos.close() + + async def test_create_cors_origin_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.user_management.cors_origins.create_cors_origin( + origin="test_origin" + ) + finally: + await workos.close() + + async def test_create_cors_origin_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.user_management.cors_origins.create_cors_origin( + origin="test_origin" + ) + finally: + await workos.close() diff --git a/tests/test_user_management_data_providers.py b/tests/test_user_management_data_providers.py new file mode 100644 index 00000000..47b99a9b --- /dev/null +++ b/tests/test_user_management_data_providers.py @@ -0,0 +1,269 @@ +# This file is auto-generated by oagen. Do not edit. + + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.user_management.data_providers.models import ( + ConnectedAccount, + DataIntegrationsListResponse, +) +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestUserManagementDataProviders: + def test_get_user_data_installation(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("connected_account.json"), + ) + result = workos.user_management.data_providers.get_user_data_installation( + "test_user_id", "test_slug" + ) + assert isinstance(result, ConnectedAccount) + assert result.object == "connected_account" + assert result.id == "data_installation_01EHZNVPK3SFK441A1RGBFSHRT" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith( + "/user_management/users/test_user_id/connected_accounts/test_slug" + ) + + def test_get_user_data_installation_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("connected_account.json")) + workos.user_management.data_providers.get_user_data_installation( + "test_user_id", "test_slug", organization_id="value organization_id/test" + ) + request = httpx_mock.get_request() + assert request.url.params["organization_id"] == "value organization_id/test" + + def test_delete_user_data_installation(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.user_management.data_providers.delete_user_data_installation( + "test_user_id", "test_slug" + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith( + "/user_management/users/test_user_id/connected_accounts/test_slug" + ) + + def test_delete_user_data_installation_encodes_query_params( + self, workos, httpx_mock + ): + httpx_mock.add_response(status_code=204) + workos.user_management.data_providers.delete_user_data_installation( + "test_user_id", "test_slug", organization_id="value organization_id/test" + ) + request = httpx_mock.get_request() + assert request.url.params["organization_id"] == "value organization_id/test" + + def test_get_user_data_integration(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("data_integrations_list_response.json"), + ) + result = workos.user_management.data_providers.get_user_data_integration( + "test_user_id" + ) + assert isinstance(result, DataIntegrationsListResponse) + assert result.object == "list" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith( + "/user_management/users/test_user_id/data_providers" + ) + + def test_get_user_data_integration_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("data_integrations_list_response.json") + ) + workos.user_management.data_providers.get_user_data_integration( + "test_user_id", organization_id="value organization_id/test" + ) + request = httpx_mock.get_request() + assert request.url.params["organization_id"] == "value organization_id/test" + + def test_get_user_data_installation_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.user_management.data_providers.get_user_data_installation( + "test_user_id", "test_slug" + ) + + def test_get_user_data_installation_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.user_management.data_providers.get_user_data_installation( + "test_user_id", "test_slug" + ) + finally: + workos.close() + + def test_get_user_data_installation_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.user_management.data_providers.get_user_data_installation( + "test_user_id", "test_slug" + ) + finally: + workos.close() + + def test_get_user_data_installation_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.user_management.data_providers.get_user_data_installation( + "test_user_id", "test_slug" + ) + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncUserManagementDataProviders: + async def test_get_user_data_installation(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("connected_account.json")) + result = await async_workos.user_management.data_providers.get_user_data_installation( + "test_user_id", "test_slug" + ) + assert isinstance(result, ConnectedAccount) + assert result.object == "connected_account" + assert result.id == "data_installation_01EHZNVPK3SFK441A1RGBFSHRT" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith( + "/user_management/users/test_user_id/connected_accounts/test_slug" + ) + + async def test_get_user_data_installation_encodes_query_params( + self, async_workos, httpx_mock + ): + httpx_mock.add_response(json=load_fixture("connected_account.json")) + await async_workos.user_management.data_providers.get_user_data_installation( + "test_user_id", "test_slug", organization_id="value organization_id/test" + ) + request = httpx_mock.get_request() + assert request.url.params["organization_id"] == "value organization_id/test" + + async def test_delete_user_data_installation(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.user_management.data_providers.delete_user_data_installation( + "test_user_id", "test_slug" + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith( + "/user_management/users/test_user_id/connected_accounts/test_slug" + ) + + async def test_delete_user_data_installation_encodes_query_params( + self, async_workos, httpx_mock + ): + httpx_mock.add_response(status_code=204) + await async_workos.user_management.data_providers.delete_user_data_installation( + "test_user_id", "test_slug", organization_id="value organization_id/test" + ) + request = httpx_mock.get_request() + assert request.url.params["organization_id"] == "value organization_id/test" + + async def test_get_user_data_integration(self, async_workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("data_integrations_list_response.json") + ) + result = ( + await async_workos.user_management.data_providers.get_user_data_integration( + "test_user_id" + ) + ) + assert isinstance(result, DataIntegrationsListResponse) + assert result.object == "list" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith( + "/user_management/users/test_user_id/data_providers" + ) + + async def test_get_user_data_integration_encodes_query_params( + self, async_workos, httpx_mock + ): + httpx_mock.add_response( + json=load_fixture("data_integrations_list_response.json") + ) + await async_workos.user_management.data_providers.get_user_data_integration( + "test_user_id", organization_id="value organization_id/test" + ) + request = httpx_mock.get_request() + assert request.url.params["organization_id"] == "value organization_id/test" + + async def test_get_user_data_installation_unauthorized( + self, async_workos, httpx_mock + ): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await ( + async_workos.user_management.data_providers.get_user_data_installation( + "test_user_id", "test_slug" + ) + ) + + async def test_get_user_data_installation_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.user_management.data_providers.get_user_data_installation( + "test_user_id", "test_slug" + ) + finally: + await workos.close() + + async def test_get_user_data_installation_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.user_management.data_providers.get_user_data_installation( + "test_user_id", "test_slug" + ) + finally: + await workos.close() + + async def test_get_user_data_installation_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.user_management.data_providers.get_user_data_installation( + "test_user_id", "test_slug" + ) + finally: + await workos.close() diff --git a/tests/test_user_management_invitations.py b/tests/test_user_management_invitations.py new file mode 100644 index 00000000..911acdab --- /dev/null +++ b/tests/test_user_management_invitations.py @@ -0,0 +1,310 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.user_management.invitations.models import ( + Invitation, + UserInvite, + UserManagementInvitationsOrder, +) +from workos._pagination import AsyncPage, SyncPage +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestUserManagementInvitations: + def test_list(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_user_invite.json"), + ) + page = workos.user_management.invitations.list() + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.user_management.invitations.list() + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.user_management.invitations.list( + limit=10, + before="cursor before", + after="cursor/after", + order=UserManagementInvitationsOrder("normal"), + organization_id="value organization_id/test", + email="value email/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["organization_id"] == "value organization_id/test" + assert request.url.params["email"] == "value email/test" + + def test_create(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("user_invite.json"), + ) + result = workos.user_management.invitations.create(email="test_email") + assert isinstance(result, UserInvite) + assert result.object == "invitation" + assert result.id == "invitation_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/invitations") + body = json.loads(request.content) + assert body["email"] == "test_email" + + def test_get_by_token(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("user_invite.json"), + ) + result = workos.user_management.invitations.get_by_token("test_token") + assert isinstance(result, UserInvite) + assert result.object == "invitation" + assert result.id == "invitation_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith( + "/user_management/invitations/by_token/test_token" + ) + + def test_get(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("user_invite.json"), + ) + result = workos.user_management.invitations.get("test_id") + assert isinstance(result, UserInvite) + assert result.object == "invitation" + assert result.id == "invitation_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/user_management/invitations/test_id") + + def test_accept(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("invitation.json"), + ) + result = workos.user_management.invitations.accept("test_id") + assert isinstance(result, Invitation) + assert result.object == "invitation" + assert result.id == "invitation_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/invitations/test_id/accept") + + def test_resend(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("user_invite.json"), + ) + result = workos.user_management.invitations.resend("test_id") + assert isinstance(result, UserInvite) + assert result.object == "invitation" + assert result.id == "invitation_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/invitations/test_id/resend") + + def test_revoke(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("invitation.json"), + ) + result = workos.user_management.invitations.revoke("test_id") + assert isinstance(result, Invitation) + assert result.object == "invitation" + assert result.id == "invitation_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/invitations/test_id/revoke") + + def test_list_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.user_management.invitations.list() + + def test_list_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.user_management.invitations.list() + finally: + workos.close() + + def test_list_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.user_management.invitations.list() + finally: + workos.close() + + def test_list_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.user_management.invitations.list() + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncUserManagementInvitations: + async def test_list(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_user_invite.json")) + page = await async_workos.user_management.invitations.list() + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.user_management.invitations.list() + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.user_management.invitations.list( + limit=10, + before="cursor before", + after="cursor/after", + order=UserManagementInvitationsOrder("normal"), + organization_id="value organization_id/test", + email="value email/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["organization_id"] == "value organization_id/test" + assert request.url.params["email"] == "value email/test" + + async def test_create(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("user_invite.json")) + result = await async_workos.user_management.invitations.create( + email="test_email" + ) + assert isinstance(result, UserInvite) + assert result.object == "invitation" + assert result.id == "invitation_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/invitations") + + async def test_get_by_token(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("user_invite.json")) + result = await async_workos.user_management.invitations.get_by_token( + "test_token" + ) + assert isinstance(result, UserInvite) + assert result.object == "invitation" + assert result.id == "invitation_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith( + "/user_management/invitations/by_token/test_token" + ) + + async def test_get(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("user_invite.json")) + result = await async_workos.user_management.invitations.get("test_id") + assert isinstance(result, UserInvite) + assert result.object == "invitation" + assert result.id == "invitation_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/user_management/invitations/test_id") + + async def test_accept(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("invitation.json")) + result = await async_workos.user_management.invitations.accept("test_id") + assert isinstance(result, Invitation) + assert result.object == "invitation" + assert result.id == "invitation_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/invitations/test_id/accept") + + async def test_resend(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("user_invite.json")) + result = await async_workos.user_management.invitations.resend("test_id") + assert isinstance(result, UserInvite) + assert result.object == "invitation" + assert result.id == "invitation_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/invitations/test_id/resend") + + async def test_revoke(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("invitation.json")) + result = await async_workos.user_management.invitations.revoke("test_id") + assert isinstance(result, Invitation) + assert result.object == "invitation" + assert result.id == "invitation_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/invitations/test_id/revoke") + + async def test_list_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.user_management.invitations.list() + + async def test_list_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.user_management.invitations.list() + finally: + await workos.close() + + async def test_list_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.user_management.invitations.list() + finally: + await workos.close() + + async def test_list_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.user_management.invitations.list() + finally: + await workos.close() diff --git a/tests/test_user_management_jwt_template.py b/tests/test_user_management_jwt_template.py new file mode 100644 index 00000000..00f889de --- /dev/null +++ b/tests/test_user_management_jwt_template.py @@ -0,0 +1,145 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.user_management.jwt_template.models import JWTTemplateResponse +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestUserManagementJWTTemplate: + def test_update_jwt_template(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("jwt_template_response.json"), + ) + result = workos.user_management.jwt_template.update_jwt_template( + content="test_content" + ) + assert isinstance(result, JWTTemplateResponse) + assert result.object == "jwt_template" + assert result.created_at == "2026-01-15T12:00:00.000Z" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/user_management/jwt_template") + body = json.loads(request.content) + assert body["content"] == "test_content" + + def test_update_jwt_template_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.user_management.jwt_template.update_jwt_template( + content="test_content" + ) + + def test_update_jwt_template_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.user_management.jwt_template.update_jwt_template( + content="test_content" + ) + finally: + workos.close() + + def test_update_jwt_template_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.user_management.jwt_template.update_jwt_template( + content="test_content" + ) + finally: + workos.close() + + def test_update_jwt_template_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.user_management.jwt_template.update_jwt_template( + content="test_content" + ) + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncUserManagementJWTTemplate: + async def test_update_jwt_template(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("jwt_template_response.json")) + result = await async_workos.user_management.jwt_template.update_jwt_template( + content="test_content" + ) + assert isinstance(result, JWTTemplateResponse) + assert result.object == "jwt_template" + assert result.created_at == "2026-01-15T12:00:00.000Z" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/user_management/jwt_template") + + async def test_update_jwt_template_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.user_management.jwt_template.update_jwt_template( + content="test_content" + ) + + async def test_update_jwt_template_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.user_management.jwt_template.update_jwt_template( + content="test_content" + ) + finally: + await workos.close() + + async def test_update_jwt_template_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.user_management.jwt_template.update_jwt_template( + content="test_content" + ) + finally: + await workos.close() + + async def test_update_jwt_template_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.user_management.jwt_template.update_jwt_template( + content="test_content" + ) + finally: + await workos.close() diff --git a/tests/test_user_management_magic_auth.py b/tests/test_user_management_magic_auth.py new file mode 100644 index 00000000..b23f9a2c --- /dev/null +++ b/tests/test_user_management_magic_auth.py @@ -0,0 +1,171 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.user_management.magic_auth.models import MagicAuth +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestUserManagementMagicAuth: + def test_send_magic_auth_code_and_return(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("magic_auth.json"), + ) + result = workos.user_management.magic_auth.send_magic_auth_code_and_return( + email="test_email" + ) + assert isinstance(result, MagicAuth) + assert result.object == "magic_auth" + assert result.id == "magic_auth_01HWZBQZY2M3AMQW166Q22K88F" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/magic_auth") + body = json.loads(request.content) + assert body["email"] == "test_email" + + def test_get(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("magic_auth.json"), + ) + result = workos.user_management.magic_auth.get("test_id") + assert isinstance(result, MagicAuth) + assert result.object == "magic_auth" + assert result.id == "magic_auth_01HWZBQZY2M3AMQW166Q22K88F" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/user_management/magic_auth/test_id") + + def test_send_magic_auth_code_and_return_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.user_management.magic_auth.send_magic_auth_code_and_return( + email="test_email" + ) + + def test_send_magic_auth_code_and_return_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.user_management.magic_auth.send_magic_auth_code_and_return( + email="test_email" + ) + finally: + workos.close() + + def test_send_magic_auth_code_and_return_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.user_management.magic_auth.send_magic_auth_code_and_return( + email="test_email" + ) + finally: + workos.close() + + def test_send_magic_auth_code_and_return_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.user_management.magic_auth.send_magic_auth_code_and_return( + email="test_email" + ) + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncUserManagementMagicAuth: + async def test_send_magic_auth_code_and_return(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("magic_auth.json")) + result = await async_workos.user_management.magic_auth.send_magic_auth_code_and_return( + email="test_email" + ) + assert isinstance(result, MagicAuth) + assert result.object == "magic_auth" + assert result.id == "magic_auth_01HWZBQZY2M3AMQW166Q22K88F" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/magic_auth") + + async def test_get(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("magic_auth.json")) + result = await async_workos.user_management.magic_auth.get("test_id") + assert isinstance(result, MagicAuth) + assert result.object == "magic_auth" + assert result.id == "magic_auth_01HWZBQZY2M3AMQW166Q22K88F" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/user_management/magic_auth/test_id") + + async def test_send_magic_auth_code_and_return_unauthorized( + self, async_workos, httpx_mock + ): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await ( + async_workos.user_management.magic_auth.send_magic_auth_code_and_return( + email="test_email" + ) + ) + + async def test_send_magic_auth_code_and_return_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.user_management.magic_auth.send_magic_auth_code_and_return( + email="test_email" + ) + finally: + await workos.close() + + async def test_send_magic_auth_code_and_return_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.user_management.magic_auth.send_magic_auth_code_and_return( + email="test_email" + ) + finally: + await workos.close() + + async def test_send_magic_auth_code_and_return_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.user_management.magic_auth.send_magic_auth_code_and_return( + email="test_email" + ) + finally: + await workos.close() diff --git a/tests/test_user_management_multi_factor_authentication.py b/tests/test_user_management_multi_factor_authentication.py new file mode 100644 index 00000000..f4af822e --- /dev/null +++ b/tests/test_user_management_multi_factor_authentication.py @@ -0,0 +1,215 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.user_management.multi_factor_authentication.models import ( + UserAuthenticationFactorEnrollResponse, + UserManagementMultiFactorAuthenticationOrder, +) +from workos._pagination import AsyncPage, SyncPage +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestUserManagementMultiFactorAuthentication: + def test_list(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_authentication_factor.json"), + ) + page = workos.user_management.multi_factor_authentication.list( + "test_userlandUserId" + ) + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.user_management.multi_factor_authentication.list( + "test_userlandUserId" + ) + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.user_management.multi_factor_authentication.list( + "test_userlandUserId", + limit=10, + before="cursor before", + after="cursor/after", + order=UserManagementMultiFactorAuthenticationOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + def test_create(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("user_authentication_factor_enroll_response.json"), + ) + result = workos.user_management.multi_factor_authentication.create( + "test_userlandUserId", type="totp" + ) + assert isinstance(result, UserAuthenticationFactorEnrollResponse) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith( + "/user_management/users/test_userlandUserId/auth_factors" + ) + body = json.loads(request.content) + assert body["type"] == "totp" + + def test_list_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.user_management.multi_factor_authentication.list( + "test_userlandUserId" + ) + + def test_list_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.user_management.multi_factor_authentication.list( + "test_userlandUserId" + ) + finally: + workos.close() + + def test_list_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.user_management.multi_factor_authentication.list( + "test_userlandUserId" + ) + finally: + workos.close() + + def test_list_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.user_management.multi_factor_authentication.list( + "test_userlandUserId" + ) + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncUserManagementMultiFactorAuthentication: + async def test_list(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_authentication_factor.json")) + page = await async_workos.user_management.multi_factor_authentication.list( + "test_userlandUserId" + ) + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.user_management.multi_factor_authentication.list( + "test_userlandUserId" + ) + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.user_management.multi_factor_authentication.list( + "test_userlandUserId", + limit=10, + before="cursor before", + after="cursor/after", + order=UserManagementMultiFactorAuthenticationOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + async def test_create(self, async_workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("user_authentication_factor_enroll_response.json") + ) + result = await async_workos.user_management.multi_factor_authentication.create( + "test_userlandUserId", type="totp" + ) + assert isinstance(result, UserAuthenticationFactorEnrollResponse) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith( + "/user_management/users/test_userlandUserId/auth_factors" + ) + + async def test_list_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.user_management.multi_factor_authentication.list( + "test_userlandUserId" + ) + + async def test_list_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.user_management.multi_factor_authentication.list( + "test_userlandUserId" + ) + finally: + await workos.close() + + async def test_list_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.user_management.multi_factor_authentication.list( + "test_userlandUserId" + ) + finally: + await workos.close() + + async def test_list_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.user_management.multi_factor_authentication.list( + "test_userlandUserId" + ) + finally: + await workos.close() diff --git a/tests/test_user_management_organization_membership.py b/tests/test_user_management_organization_membership.py new file mode 100644 index 00000000..fd7fc4ea --- /dev/null +++ b/tests/test_user_management_organization_membership.py @@ -0,0 +1,333 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.user_management.organization_membership.models import ( + OrganizationMembership, + UserOrganizationMembership, + UserManagementOrganizationMembershipOrder, +) +from workos._pagination import AsyncPage, SyncPage +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestUserManagementOrganizationMembership: + def test_list(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_user_organization_membership.json"), + ) + page = workos.user_management.organization_membership.list() + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.user_management.organization_membership.list() + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.user_management.organization_membership.list( + limit=10, + before="cursor before", + after="cursor/after", + order=UserManagementOrganizationMembershipOrder("normal"), + organization_id="value organization_id/test", + user_id="value user_id/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["organization_id"] == "value organization_id/test" + assert request.url.params["user_id"] == "value user_id/test" + + def test_create(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("organization_membership.json"), + ) + result = workos.user_management.organization_membership.create( + user_id="test_user_id", organization_id="test_organization_id" + ) + assert isinstance(result, OrganizationMembership) + assert result.object == "organization_membership" + assert result.id == "om_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/organization_memberships") + body = json.loads(request.content) + assert body["user_id"] == "test_user_id" + assert body["organization_id"] == "test_organization_id" + + def test_get(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("user_organization_membership.json"), + ) + result = workos.user_management.organization_membership.get("test_id") + assert isinstance(result, UserOrganizationMembership) + assert result.object == "organization_membership" + assert result.id == "om_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith( + "/user_management/organization_memberships/test_id" + ) + + def test_update(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("user_organization_membership.json"), + ) + result = workos.user_management.organization_membership.update("test_id") + assert isinstance(result, UserOrganizationMembership) + assert result.object == "organization_membership" + assert result.id == "om_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith( + "/user_management/organization_memberships/test_id" + ) + + def test_delete(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.user_management.organization_membership.delete("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith( + "/user_management/organization_memberships/test_id" + ) + + def test_deactivate(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("organization_membership.json"), + ) + result = workos.user_management.organization_membership.deactivate("test_id") + assert isinstance(result, OrganizationMembership) + assert result.object == "organization_membership" + assert result.id == "om_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith( + "/user_management/organization_memberships/test_id/deactivate" + ) + + def test_reactivate(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("user_organization_membership.json"), + ) + result = workos.user_management.organization_membership.reactivate("test_id") + assert isinstance(result, UserOrganizationMembership) + assert result.object == "organization_membership" + assert result.id == "om_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith( + "/user_management/organization_memberships/test_id/reactivate" + ) + + def test_list_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.user_management.organization_membership.list() + + def test_list_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.user_management.organization_membership.list() + finally: + workos.close() + + def test_list_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.user_management.organization_membership.list() + finally: + workos.close() + + def test_list_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.user_management.organization_membership.list() + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncUserManagementOrganizationMembership: + async def test_list(self, async_workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_user_organization_membership.json") + ) + page = await async_workos.user_management.organization_membership.list() + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.user_management.organization_membership.list() + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.user_management.organization_membership.list( + limit=10, + before="cursor before", + after="cursor/after", + order=UserManagementOrganizationMembershipOrder("normal"), + organization_id="value organization_id/test", + user_id="value user_id/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["organization_id"] == "value organization_id/test" + assert request.url.params["user_id"] == "value user_id/test" + + async def test_create(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("organization_membership.json")) + result = await async_workos.user_management.organization_membership.create( + user_id="test_user_id", organization_id="test_organization_id" + ) + assert isinstance(result, OrganizationMembership) + assert result.object == "organization_membership" + assert result.id == "om_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/organization_memberships") + + async def test_get(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("user_organization_membership.json")) + result = await async_workos.user_management.organization_membership.get( + "test_id" + ) + assert isinstance(result, UserOrganizationMembership) + assert result.object == "organization_membership" + assert result.id == "om_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith( + "/user_management/organization_memberships/test_id" + ) + + async def test_update(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("user_organization_membership.json")) + result = await async_workos.user_management.organization_membership.update( + "test_id" + ) + assert isinstance(result, UserOrganizationMembership) + assert result.object == "organization_membership" + assert result.id == "om_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith( + "/user_management/organization_memberships/test_id" + ) + + async def test_delete(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.user_management.organization_membership.delete( + "test_id" + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith( + "/user_management/organization_memberships/test_id" + ) + + async def test_deactivate(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("organization_membership.json")) + result = await async_workos.user_management.organization_membership.deactivate( + "test_id" + ) + assert isinstance(result, OrganizationMembership) + assert result.object == "organization_membership" + assert result.id == "om_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith( + "/user_management/organization_memberships/test_id/deactivate" + ) + + async def test_reactivate(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("user_organization_membership.json")) + result = await async_workos.user_management.organization_membership.reactivate( + "test_id" + ) + assert isinstance(result, UserOrganizationMembership) + assert result.object == "organization_membership" + assert result.id == "om_01HXYZ123456789ABCDEFGHIJ" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith( + "/user_management/organization_memberships/test_id/reactivate" + ) + + async def test_list_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.user_management.organization_membership.list() + + async def test_list_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.user_management.organization_membership.list() + finally: + await workos.close() + + async def test_list_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.user_management.organization_membership.list() + finally: + await workos.close() + + async def test_list_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.user_management.organization_membership.list() + finally: + await workos.close() diff --git a/tests/test_user_management_redirect_uris.py b/tests/test_user_management_redirect_uris.py new file mode 100644 index 00000000..fa563d3b --- /dev/null +++ b/tests/test_user_management_redirect_uris.py @@ -0,0 +1,125 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.user_management.redirect_uris.models import RedirectUri +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestUserManagementRedirectUris: + def test_create(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("redirect_uri.json"), + ) + result = workos.user_management.redirect_uris.create(uri="test_uri") + assert isinstance(result, RedirectUri) + assert result.object == "redirect_uri" + assert result.id == "ruri_01EHZNVPK3SFK441A1RGBFSHRT" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/redirect_uris") + body = json.loads(request.content) + assert body["uri"] == "test_uri" + + def test_create_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.user_management.redirect_uris.create(uri="test_uri") + + def test_create_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.user_management.redirect_uris.create(uri="test_uri") + finally: + workos.close() + + def test_create_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.user_management.redirect_uris.create(uri="test_uri") + finally: + workos.close() + + def test_create_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.user_management.redirect_uris.create(uri="test_uri") + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncUserManagementRedirectUris: + async def test_create(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("redirect_uri.json")) + result = await async_workos.user_management.redirect_uris.create(uri="test_uri") + assert isinstance(result, RedirectUri) + assert result.object == "redirect_uri" + assert result.id == "ruri_01EHZNVPK3SFK441A1RGBFSHRT" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/redirect_uris") + + async def test_create_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.user_management.redirect_uris.create(uri="test_uri") + + async def test_create_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.user_management.redirect_uris.create(uri="test_uri") + finally: + await workos.close() + + async def test_create_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.user_management.redirect_uris.create(uri="test_uri") + finally: + await workos.close() + + async def test_create_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.user_management.redirect_uris.create(uri="test_uri") + finally: + await workos.close() diff --git a/tests/test_user_management_session_tokens.py b/tests/test_user_management_session_tokens.py new file mode 100644 index 00000000..3ada7332 --- /dev/null +++ b/tests/test_user_management_session_tokens.py @@ -0,0 +1,128 @@ +# This file is auto-generated by oagen. Do not edit. + + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.user_management.session_tokens.models import JwksResponse +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestUserManagementSessionTokens: + def test_json_web_key_set(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("jwks_response.json"), + ) + result = workos.user_management.session_tokens.json_web_key_set("test_clientId") + assert isinstance(result, JwksResponse) + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/sso/jwks/test_clientId") + + def test_json_web_key_set_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.user_management.session_tokens.json_web_key_set("test_clientId") + + def test_json_web_key_set_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.user_management.session_tokens.json_web_key_set("test_clientId") + finally: + workos.close() + + def test_json_web_key_set_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.user_management.session_tokens.json_web_key_set("test_clientId") + finally: + workos.close() + + def test_json_web_key_set_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.user_management.session_tokens.json_web_key_set("test_clientId") + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncUserManagementSessionTokens: + async def test_json_web_key_set(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("jwks_response.json")) + result = await async_workos.user_management.session_tokens.json_web_key_set( + "test_clientId" + ) + assert isinstance(result, JwksResponse) + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/sso/jwks/test_clientId") + + async def test_json_web_key_set_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.user_management.session_tokens.json_web_key_set( + "test_clientId" + ) + + async def test_json_web_key_set_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.user_management.session_tokens.json_web_key_set( + "test_clientId" + ) + finally: + await workos.close() + + async def test_json_web_key_set_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.user_management.session_tokens.json_web_key_set( + "test_clientId" + ) + finally: + await workos.close() + + async def test_json_web_key_set_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.user_management.session_tokens.json_web_key_set( + "test_clientId" + ) + finally: + await workos.close() diff --git a/tests/test_user_management_users.py b/tests/test_user_management_users.py new file mode 100644 index 00000000..0ba25941 --- /dev/null +++ b/tests/test_user_management_users.py @@ -0,0 +1,512 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.user_management.authentication.models import User +from workos.user_management.users.models import ( + EmailVerification, + PasswordReset, + ResetPasswordResponse, + SendVerificationEmailResponse, + UserIdentitiesGetItem, + VerifyEmailResponse, + UserManagementUsersOrder, +) +from workos._pagination import AsyncPage, SyncPage +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestUserManagementUsers: + def test_get_email_verification(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("email_verification.json"), + ) + result = workos.user_management.users.get_email_verification("test_id") + assert isinstance(result, EmailVerification) + assert result.object == "email_verification" + assert result.id == "email_verification_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/user_management/email_verification/test_id") + + def test_create_password_reset_token(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("password_reset.json"), + ) + result = workos.user_management.users.create_password_reset_token( + email="test_email" + ) + assert isinstance(result, PasswordReset) + assert result.object == "password_reset" + assert result.id == "password_reset_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/password_reset") + body = json.loads(request.content) + assert body["email"] == "test_email" + + def test_reset_password(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("reset_password_response.json"), + ) + result = workos.user_management.users.reset_password( + token="test_token", new_password="test_new_password" + ) + assert isinstance(result, ResetPasswordResponse) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/password_reset/confirm") + body = json.loads(request.content) + assert body["token"] == "test_token" + assert body["new_password"] == "test_new_password" + + def test_get_password_reset(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("password_reset.json"), + ) + result = workos.user_management.users.get_password_reset("test_id") + assert isinstance(result, PasswordReset) + assert result.object == "password_reset" + assert result.id == "password_reset_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/user_management/password_reset/test_id") + + def test_list_users(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_user.json"), + ) + page = workos.user_management.users.list_users() + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_users_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.user_management.users.list_users() + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_users_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.user_management.users.list_users( + limit=10, + before="cursor before", + after="cursor/after", + order=UserManagementUsersOrder("normal"), + organization="value organization/test", + organization_id="value organization_id/test", + email="value email/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["organization"] == "value organization/test" + assert request.url.params["organization_id"] == "value organization_id/test" + assert request.url.params["email"] == "value email/test" + + def test_create(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("user.json"), + ) + result = workos.user_management.users.create(email="test_email") + assert isinstance(result, User) + assert result.object == "user" + assert result.id == "user_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/users") + body = json.loads(request.content) + assert body["email"] == "test_email" + + def test_get_by_external_id(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("user.json"), + ) + result = workos.user_management.users.get_by_external_id("test_external_id") + assert isinstance(result, User) + assert result.object == "user" + assert result.id == "user_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith( + "/user_management/users/external_id/test_external_id" + ) + + def test_get_user(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("user.json"), + ) + result = workos.user_management.users.get_user("test_id") + assert isinstance(result, User) + assert result.object == "user" + assert result.id == "user_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/user_management/users/test_id") + + def test_update(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("user.json"), + ) + result = workos.user_management.users.update("test_id") + assert isinstance(result, User) + assert result.object == "user" + assert result.id == "user_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/user_management/users/test_id") + + def test_delete(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.user_management.users.delete("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/user_management/users/test_id") + + def test_email_verification(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("verify_email_response.json"), + ) + result = workos.user_management.users.email_verification( + "test_id", code="test_code" + ) + assert isinstance(result, VerifyEmailResponse) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith( + "/user_management/users/test_id/email_verification/confirm" + ) + body = json.loads(request.content) + assert body["code"] == "test_code" + + def test_send_verification_email(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("send_verification_email_response.json"), + ) + result = workos.user_management.users.send_verification_email("test_id") + assert isinstance(result, SendVerificationEmailResponse) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith( + "/user_management/users/test_id/email_verification/send" + ) + + def test_get_identity(self, workos, httpx_mock): + httpx_mock.add_response(json=[load_fixture("user_identities_get_item.json")]) + result = workos.user_management.users.get_identity("test_id") + assert isinstance(result, list) + assert len(result) == 1 + assert isinstance(result[0], UserIdentitiesGetItem) + + def test_list_sessions(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_user_sessions_list_item.json"), + ) + page = workos.user_management.users.list_sessions("test_id") + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_sessions_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.user_management.users.list_sessions("test_id") + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_sessions_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.user_management.users.list_sessions( + "test_id", + limit=10, + before="cursor before", + after="cursor/after", + order=UserManagementUsersOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + def test_get_email_verification_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.user_management.users.get_email_verification("test_id") + + def test_get_email_verification_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.user_management.users.get_email_verification("test_id") + finally: + workos.close() + + def test_get_email_verification_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.user_management.users.get_email_verification("test_id") + finally: + workos.close() + + def test_get_email_verification_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.user_management.users.get_email_verification("test_id") + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncUserManagementUsers: + async def test_get_email_verification(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("email_verification.json")) + result = await async_workos.user_management.users.get_email_verification( + "test_id" + ) + assert isinstance(result, EmailVerification) + assert result.object == "email_verification" + assert result.id == "email_verification_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/user_management/email_verification/test_id") + + async def test_create_password_reset_token(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("password_reset.json")) + result = await async_workos.user_management.users.create_password_reset_token( + email="test_email" + ) + assert isinstance(result, PasswordReset) + assert result.object == "password_reset" + assert result.id == "password_reset_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/password_reset") + + async def test_reset_password(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("reset_password_response.json")) + result = await async_workos.user_management.users.reset_password( + token="test_token", new_password="test_new_password" + ) + assert isinstance(result, ResetPasswordResponse) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/password_reset/confirm") + + async def test_get_password_reset(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("password_reset.json")) + result = await async_workos.user_management.users.get_password_reset("test_id") + assert isinstance(result, PasswordReset) + assert result.object == "password_reset" + assert result.id == "password_reset_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/user_management/password_reset/test_id") + + async def test_list_users(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_user.json")) + page = await async_workos.user_management.users.list_users() + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_users_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.user_management.users.list_users() + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_users_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.user_management.users.list_users( + limit=10, + before="cursor before", + after="cursor/after", + order=UserManagementUsersOrder("normal"), + organization="value organization/test", + organization_id="value organization_id/test", + email="value email/test", + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + assert request.url.params["organization"] == "value organization/test" + assert request.url.params["organization_id"] == "value organization_id/test" + assert request.url.params["email"] == "value email/test" + + async def test_create(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("user.json")) + result = await async_workos.user_management.users.create(email="test_email") + assert isinstance(result, User) + assert result.object == "user" + assert result.id == "user_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/user_management/users") + + async def test_get_by_external_id(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("user.json")) + result = await async_workos.user_management.users.get_by_external_id( + "test_external_id" + ) + assert isinstance(result, User) + assert result.object == "user" + assert result.id == "user_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith( + "/user_management/users/external_id/test_external_id" + ) + + async def test_get_user(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("user.json")) + result = await async_workos.user_management.users.get_user("test_id") + assert isinstance(result, User) + assert result.object == "user" + assert result.id == "user_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "GET" + assert request.url.path.endswith("/user_management/users/test_id") + + async def test_update(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("user.json")) + result = await async_workos.user_management.users.update("test_id") + assert isinstance(result, User) + assert result.object == "user" + assert result.id == "user_01E4ZCR3C56J083X43JQXF3JK5" + request = httpx_mock.get_request() + assert request.method == "PUT" + assert request.url.path.endswith("/user_management/users/test_id") + + async def test_delete(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.user_management.users.delete("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/user_management/users/test_id") + + async def test_email_verification(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("verify_email_response.json")) + result = await async_workos.user_management.users.email_verification( + "test_id", code="test_code" + ) + assert isinstance(result, VerifyEmailResponse) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith( + "/user_management/users/test_id/email_verification/confirm" + ) + + async def test_send_verification_email(self, async_workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("send_verification_email_response.json") + ) + result = await async_workos.user_management.users.send_verification_email( + "test_id" + ) + assert isinstance(result, SendVerificationEmailResponse) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith( + "/user_management/users/test_id/email_verification/send" + ) + + async def test_get_identity(self, async_workos, httpx_mock): + httpx_mock.add_response(json=[load_fixture("user_identities_get_item.json")]) + result = await async_workos.user_management.users.get_identity("test_id") + assert isinstance(result, list) + assert len(result) == 1 + assert isinstance(result[0], UserIdentitiesGetItem) + + async def test_list_sessions(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_user_sessions_list_item.json")) + page = await async_workos.user_management.users.list_sessions("test_id") + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_sessions_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.user_management.users.list_sessions("test_id") + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_sessions_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.user_management.users.list_sessions( + "test_id", + limit=10, + before="cursor before", + after="cursor/after", + order=UserManagementUsersOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + async def test_get_email_verification_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.user_management.users.get_email_verification("test_id") + + async def test_get_email_verification_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.user_management.users.get_email_verification("test_id") + finally: + await workos.close() + + async def test_get_email_verification_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.user_management.users.get_email_verification("test_id") + finally: + await workos.close() + + async def test_get_email_verification_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.user_management.users.get_email_verification("test_id") + finally: + await workos.close() diff --git a/tests/test_user_management_users_authorized_applications.py b/tests/test_user_management_users_authorized_applications.py new file mode 100644 index 00000000..b7fdb9d3 --- /dev/null +++ b/tests/test_user_management_users_authorized_applications.py @@ -0,0 +1,205 @@ +# This file is auto-generated by oagen. Do not edit. + + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.user_management_users.authorized_applications.models import ( + UserManagementUsersAuthorizedApplicationsOrder, +) +from workos._pagination import AsyncPage, SyncPage +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestUserManagementUsersAuthorizedApplications: + def test_list(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_authorized_connect_application_list_data.json"), + ) + page = workos.user_management_users.authorized_applications.list("test_user_id") + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.user_management_users.authorized_applications.list("test_user_id") + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.user_management_users.authorized_applications.list( + "test_user_id", + limit=10, + before="cursor before", + after="cursor/after", + order=UserManagementUsersAuthorizedApplicationsOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + def test_delete(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.user_management_users.authorized_applications.delete( + "test_application_id", "test_user_id" + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith( + "/user_management/users/test_user_id/authorized_applications/test_application_id" + ) + + def test_list_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.user_management_users.authorized_applications.list("test_user_id") + + def test_list_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.user_management_users.authorized_applications.list( + "test_user_id" + ) + finally: + workos.close() + + def test_list_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.user_management_users.authorized_applications.list( + "test_user_id" + ) + finally: + workos.close() + + def test_list_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.user_management_users.authorized_applications.list( + "test_user_id" + ) + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncUserManagementUsersAuthorizedApplications: + async def test_list(self, async_workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_authorized_connect_application_list_data.json") + ) + page = await async_workos.user_management_users.authorized_applications.list( + "test_user_id" + ) + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.user_management_users.authorized_applications.list( + "test_user_id" + ) + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.user_management_users.authorized_applications.list( + "test_user_id", + limit=10, + before="cursor before", + after="cursor/after", + order=UserManagementUsersAuthorizedApplicationsOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + async def test_delete(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = ( + await async_workos.user_management_users.authorized_applications.delete( + "test_application_id", "test_user_id" + ) + ) + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith( + "/user_management/users/test_user_id/authorized_applications/test_application_id" + ) + + async def test_list_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.user_management_users.authorized_applications.list( + "test_user_id" + ) + + async def test_list_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.user_management_users.authorized_applications.list( + "test_user_id" + ) + finally: + await workos.close() + + async def test_list_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.user_management_users.authorized_applications.list( + "test_user_id" + ) + finally: + await workos.close() + + async def test_list_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.user_management_users.authorized_applications.list( + "test_user_id" + ) + finally: + await workos.close() diff --git a/tests/test_user_management_users_feature_flags.py b/tests/test_user_management_users_feature_flags.py new file mode 100644 index 00000000..50751c82 --- /dev/null +++ b/tests/test_user_management_users_feature_flags.py @@ -0,0 +1,163 @@ +# This file is auto-generated by oagen. Do not edit. + + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.user_management_users.feature_flags.models import ( + UserManagementUsersFeatureFlagsOrder, +) +from workos._pagination import AsyncPage, SyncPage +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestUserManagementUsersFeatureFlags: + def test_list(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_flag.json"), + ) + page = workos.user_management_users.feature_flags.list("test_userId") + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.user_management_users.feature_flags.list("test_userId") + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.user_management_users.feature_flags.list( + "test_userId", + limit=10, + before="cursor before", + after="cursor/after", + order=UserManagementUsersFeatureFlagsOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + def test_list_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.user_management_users.feature_flags.list("test_userId") + + def test_list_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.user_management_users.feature_flags.list("test_userId") + finally: + workos.close() + + def test_list_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.user_management_users.feature_flags.list("test_userId") + finally: + workos.close() + + def test_list_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.user_management_users.feature_flags.list("test_userId") + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncUserManagementUsersFeatureFlags: + async def test_list(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_flag.json")) + page = await async_workos.user_management_users.feature_flags.list( + "test_userId" + ) + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.user_management_users.feature_flags.list( + "test_userId" + ) + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.user_management_users.feature_flags.list( + "test_userId", + limit=10, + before="cursor before", + after="cursor/after", + order=UserManagementUsersFeatureFlagsOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + async def test_list_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.user_management_users.feature_flags.list("test_userId") + + async def test_list_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.user_management_users.feature_flags.list("test_userId") + finally: + await workos.close() + + async def test_list_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.user_management_users.feature_flags.list("test_userId") + finally: + await workos.close() + + async def test_list_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.user_management_users.feature_flags.list("test_userId") + finally: + await workos.close() diff --git a/tests/test_webhooks.py b/tests/test_webhooks.py new file mode 100644 index 00000000..997985d1 --- /dev/null +++ b/tests/test_webhooks.py @@ -0,0 +1,221 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.webhooks.models import WebhookEndpointJson, WebhooksOrder +from workos._pagination import AsyncPage, SyncPage +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestWebhooks: + def test_list(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_webhook_endpoint_json.json"), + ) + page = workos.webhooks.list() + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_empty_page(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = workos.webhooks.list() + assert isinstance(page, SyncPage) + assert page.data == [] + + def test_list_encodes_query_params(self, workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + workos.webhooks.list( + limit=10, + before="cursor before", + after="cursor/after", + order=WebhooksOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + def test_create(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("webhook_endpoint_json.json"), + ) + result = workos.webhooks.create(endpoint_url="test_endpoint_url", events=[]) + assert isinstance(result, WebhookEndpointJson) + assert result.object == "webhook_endpoint" + assert result.id == "we_0123456789" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/webhook_endpoints") + body = json.loads(request.content) + assert body["endpoint_url"] == "test_endpoint_url" + assert "events" in body + + def test_update(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("webhook_endpoint_json.json"), + ) + result = workos.webhooks.update("test_id") + assert isinstance(result, WebhookEndpointJson) + assert result.object == "webhook_endpoint" + assert result.id == "we_0123456789" + request = httpx_mock.get_request() + assert request.method == "PATCH" + assert request.url.path.endswith("/webhook_endpoints/test_id") + + def test_delete(self, workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = workos.webhooks.delete("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/webhook_endpoints/test_id") + + def test_list_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.webhooks.list() + + def test_list_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.webhooks.list() + finally: + workos.close() + + def test_list_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.webhooks.list() + finally: + workos.close() + + def test_list_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.webhooks.list() + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncWebhooks: + async def test_list(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_webhook_endpoint_json.json")) + page = await async_workos.webhooks.list() + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_empty_page(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + page = await async_workos.webhooks.list() + assert isinstance(page, AsyncPage) + assert page.data == [] + + async def test_list_encodes_query_params(self, async_workos, httpx_mock): + httpx_mock.add_response(json={"data": [], "list_metadata": {}}) + await async_workos.webhooks.list( + limit=10, + before="cursor before", + after="cursor/after", + order=WebhooksOrder("normal"), + ) + request = httpx_mock.get_request() + assert request.url.params["limit"] == "10" + assert request.url.params["before"] == "cursor before" + assert request.url.params["after"] == "cursor/after" + assert request.url.params["order"] == "normal" + + async def test_create(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("webhook_endpoint_json.json")) + result = await async_workos.webhooks.create( + endpoint_url="test_endpoint_url", events=[] + ) + assert isinstance(result, WebhookEndpointJson) + assert result.object == "webhook_endpoint" + assert result.id == "we_0123456789" + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/webhook_endpoints") + + async def test_update(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("webhook_endpoint_json.json")) + result = await async_workos.webhooks.update("test_id") + assert isinstance(result, WebhookEndpointJson) + assert result.object == "webhook_endpoint" + assert result.id == "we_0123456789" + request = httpx_mock.get_request() + assert request.method == "PATCH" + assert request.url.path.endswith("/webhook_endpoints/test_id") + + async def test_delete(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=204) + result = await async_workos.webhooks.delete("test_id") + assert result is None + request = httpx_mock.get_request() + assert request.method == "DELETE" + assert request.url.path.endswith("/webhook_endpoints/test_id") + + async def test_list_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.webhooks.list() + + async def test_list_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.webhooks.list() + finally: + await workos.close() + + async def test_list_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.webhooks.list() + finally: + await workos.close() + + async def test_list_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.webhooks.list() + finally: + await workos.close() diff --git a/tests/test_widgets.py b/tests/test_widgets.py new file mode 100644 index 00000000..b1babac9 --- /dev/null +++ b/tests/test_widgets.py @@ -0,0 +1,145 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.widgets.models import WidgetSessionTokenResponse +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestWidgets: + def test_issue_widget_session_token(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("widget_session_token_response.json"), + ) + result = workos.widgets.issue_widget_session_token( + organization_id="test_organization_id" + ) + assert isinstance(result, WidgetSessionTokenResponse) + assert result.token == "eyJhbGciOiJSUzI1NiIsImtpZCI6InNlc3Npb24..." + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/widgets/token") + body = json.loads(request.content) + assert body["organization_id"] == "test_organization_id" + + def test_issue_widget_session_token_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.widgets.issue_widget_session_token( + organization_id="test_organization_id" + ) + + def test_issue_widget_session_token_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.widgets.issue_widget_session_token( + organization_id="test_organization_id" + ) + finally: + workos.close() + + def test_issue_widget_session_token_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.widgets.issue_widget_session_token( + organization_id="test_organization_id" + ) + finally: + workos.close() + + def test_issue_widget_session_token_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.widgets.issue_widget_session_token( + organization_id="test_organization_id" + ) + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncWidgets: + async def test_issue_widget_session_token(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("widget_session_token_response.json")) + result = await async_workos.widgets.issue_widget_session_token( + organization_id="test_organization_id" + ) + assert isinstance(result, WidgetSessionTokenResponse) + assert result.token == "eyJhbGciOiJSUzI1NiIsImtpZCI6InNlc3Npb24..." + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/widgets/token") + + async def test_issue_widget_session_token_unauthorized( + self, async_workos, httpx_mock + ): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.widgets.issue_widget_session_token( + organization_id="test_organization_id" + ) + + async def test_issue_widget_session_token_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.widgets.issue_widget_session_token( + organization_id="test_organization_id" + ) + finally: + await workos.close() + + async def test_issue_widget_session_token_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.widgets.issue_widget_session_token( + organization_id="test_organization_id" + ) + finally: + await workos.close() + + async def test_issue_widget_session_token_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.widgets.issue_widget_session_token( + organization_id="test_organization_id" + ) + finally: + await workos.close() diff --git a/tests/test_workos_connect.py b/tests/test_workos_connect.py new file mode 100644 index 00000000..d69705eb --- /dev/null +++ b/tests/test_workos_connect.py @@ -0,0 +1,162 @@ +# This file is auto-generated by oagen. Do not edit. + +import json + +import pytest +from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture + +from workos.workos_connect.models import ExternalAuthCompleteResponse, UserObject +from workos._errors import ( + AuthenticationException, + NotFoundException, + RateLimitExceededException, + ServerException, +) + + +class TestWorkosConnect: + def test_complete_login(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("external_auth_complete_response.json"), + ) + result = workos.workos_connect.complete_login( + external_auth_id="test_external_auth_id", + user=UserObject.from_dict(load_fixture("user_object.json")), + ) + assert isinstance(result, ExternalAuthCompleteResponse) + assert ( + result.redirect_uri + == "https://your-authkit-domain.workos.com/oauth/authorize/complete?state=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdGF0ZSI6InJhbmRvbV9zdGF0ZV9zdHJpbmciLCJpYXQiOjE3NDI2MDQ4NTN9.abc123def456ghi789" + ) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/authkit/oauth2/complete") + body = json.loads(request.content) + assert body["external_auth_id"] == "test_external_auth_id" + assert "user" in body + + def test_complete_login_unauthorized(self, workos, httpx_mock): + httpx_mock.add_response( + status_code=401, + json={"message": "Unauthorized"}, + ) + with pytest.raises(AuthenticationException): + workos.workos_connect.complete_login( + external_auth_id="test_external_auth_id", + user=UserObject.from_dict(load_fixture("user_object.json")), + ) + + def test_complete_login_not_found(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + workos.workos_connect.complete_login( + external_auth_id="test_external_auth_id", + user=UserObject.from_dict(load_fixture("user_object.json")), + ) + finally: + workos.close() + + def test_complete_login_rate_limited(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + workos.workos_connect.complete_login( + external_auth_id="test_external_auth_id", + user=UserObject.from_dict(load_fixture("user_object.json")), + ) + finally: + workos.close() + + def test_complete_login_server_error(self, httpx_mock): + workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + workos.workos_connect.complete_login( + external_auth_id="test_external_auth_id", + user=UserObject.from_dict(load_fixture("user_object.json")), + ) + finally: + workos.close() + + +@pytest.mark.asyncio +class TestAsyncWorkosConnect: + async def test_complete_login(self, async_workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("external_auth_complete_response.json") + ) + result = await async_workos.workos_connect.complete_login( + external_auth_id="test_external_auth_id", + user=UserObject.from_dict(load_fixture("user_object.json")), + ) + assert isinstance(result, ExternalAuthCompleteResponse) + assert ( + result.redirect_uri + == "https://your-authkit-domain.workos.com/oauth/authorize/complete?state=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdGF0ZSI6InJhbmRvbV9zdGF0ZV9zdHJpbmciLCJpYXQiOjE3NDI2MDQ4NTN9.abc123def456ghi789" + ) + request = httpx_mock.get_request() + assert request.method == "POST" + assert request.url.path.endswith("/authkit/oauth2/complete") + + async def test_complete_login_unauthorized(self, async_workos, httpx_mock): + httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) + with pytest.raises(AuthenticationException): + await async_workos.workos_connect.complete_login( + external_auth_id="test_external_auth_id", + user=UserObject.from_dict(load_fixture("user_object.json")), + ) + + async def test_complete_login_not_found(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=404, json={"message": "Not found"}) + with pytest.raises(NotFoundException): + await workos.workos_connect.complete_login( + external_auth_id="test_external_auth_id", + user=UserObject.from_dict(load_fixture("user_object.json")), + ) + finally: + await workos.close() + + async def test_complete_login_rate_limited(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response( + status_code=429, + headers={"Retry-After": "0"}, + json={"message": "Slow down"}, + ) + with pytest.raises(RateLimitExceededException): + await workos.workos_connect.complete_login( + external_auth_id="test_external_auth_id", + user=UserObject.from_dict(load_fixture("user_object.json")), + ) + finally: + await workos.close() + + async def test_complete_login_server_error(self, httpx_mock): + workos = AsyncWorkOS( + api_key="sk_test_123", client_id="client_test", max_retries=0 + ) + try: + httpx_mock.add_response(status_code=500, json={"message": "Server error"}) + with pytest.raises(ServerException): + await workos.workos_connect.complete_login( + external_auth_id="test_external_auth_id", + user=UserObject.from_dict(load_fixture("user_object.json")), + ) + finally: + await workos.close() From 2e21112345cc4f9c6fecd8b3dd0617b12d81b72e Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 31 Mar 2026 13:37:17 -0400 Subject: [PATCH 14/26] lock cleanup --- pyproject.toml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 776ea504..7dfcfbda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,11 +7,7 @@ license = "MIT" authors = [{ name = "WorkOS", email = "sdk@workos.com" }] requires-python = ">=3.10" -dependencies = [ - "cryptography~=46.0", - "httpx~=0.28", - "pyjwt~=2.12", -] +dependencies = ["cryptography~=46.0", "httpx~=0.28", "pyjwt~=2.12"] [project.urls] Homepage = "https://workos.com/docs/sdks/python" @@ -24,14 +20,14 @@ dev = [ { include-group = "lint" }, { include-group = "type_check" }, { include-group = "nox" }, - "pyright>=1.1.408", + "pyright~=1.1", ] test = [ "pytest~=9.0", "pytest-asyncio~=1.3", "pytest-cov~=7.1", "pytest-httpx~=0.36", - ] +] lint = ["ruff~=0.15"] type_check = ["pyright~=1.1"] nox = ["nox~=2026.2", "nox-uv~=0.7"] From 556cba69d96dbb2b8834a4068f839335af5faa87 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 31 Mar 2026 14:37:55 -0400 Subject: [PATCH 15/26] format what needs formatting --- src/workos/sso/_resource.py | 4 +++- src/workos/webhooks/_resource.py | 17 +++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/workos/sso/_resource.py b/src/workos/sso/_resource.py index 0a628cc1..3cb2fa4f 100644 --- a/src/workos/sso/_resource.py +++ b/src/workos/sso/_resource.py @@ -9,9 +9,9 @@ from .models import Profile, SSOLogoutAuthorizeResponse, SSOTokenResponse from .models import SSOProvider +from .._types import RequestOptions from ..connections.models import Connection, ConnectionsConnectionType, ConnectionsOrder from .._pagination import AsyncPage, SyncPage -from .._types import RequestOptions class SSO: @@ -325,6 +325,7 @@ def delete_connection( request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. """ self._client.connections.delete(id, request_options=request_options) + # @oagen-ignore-end @@ -639,4 +640,5 @@ async def delete_connection( request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. """ await self._client.connections.delete(id, request_options=request_options) + # @oagen-ignore-end diff --git a/src/workos/webhooks/_resource.py b/src/workos/webhooks/_resource.py index 17317705..3f040eca 100644 --- a/src/workos/webhooks/_resource.py +++ b/src/workos/webhooks/_resource.py @@ -2,10 +2,6 @@ from __future__ import annotations -import hashlib -import hmac -import json -import time from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union if TYPE_CHECKING: @@ -20,6 +16,10 @@ ) from .._pagination import AsyncPage, SyncPage from .._types import RequestOptions +import hashlib +import hmac +import json +import time class Webhooks: @@ -249,7 +249,9 @@ def verify_header( issued_timestamp = issued_timestamp[2:] signature_hash = signature_hash[3:] - max_seconds_since_issued = tolerance if tolerance is not None else self.DEFAULT_TOLERANCE + max_seconds_since_issued = ( + tolerance if tolerance is not None else self.DEFAULT_TOLERANCE + ) current_time = time.time() timestamp_in_seconds = int(issued_timestamp) / 1000 seconds_since_issued = current_time - timestamp_in_seconds @@ -258,9 +260,7 @@ def verify_header( raise ValueError("Timestamp outside the tolerance zone") body_str = ( - event_body.decode("utf-8") - if isinstance(event_body, bytes) - else event_body + event_body.decode("utf-8") if isinstance(event_body, bytes) else event_body ) unhashed_string = f"{issued_timestamp}.{body_str}" expected_signature = hmac.new( @@ -273,6 +273,7 @@ def verify_header( raise ValueError( "Signature hash does not match the expected signature hash for payload" ) + # @oagen-ignore-end From 62bb01ebfa01a1e1f6d01b3f9b7f59496e7c7070 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 31 Mar 2026 14:39:33 -0400 Subject: [PATCH 16/26] remove these weird aliases --- src/workos/api_keys/_resource.py | 8 +-- src/workos/applications/_resource.py | 4 -- src/workos/audit_logs/_resource.py | 12 ---- src/workos/authorization/_resource.py | 24 ------- src/workos/connections/_resource.py | 4 -- src/workos/directories/_resource.py | 8 --- src/workos/directory_groups/_resource.py | 4 -- src/workos/directory_users/_resource.py | 4 -- src/workos/events/_resource.py | 4 -- src/workos/feature_flags/_resource.py | 4 -- src/workos/organization_domains/_resource.py | 28 ++------- src/workos/organizations/_resource.py | 4 -- src/workos/permissions/_resource.py | 4 -- src/workos/sso/_resource.py | 4 -- .../data_providers/_resource.py | 4 -- src/workos/user_management/users/_resource.py | 8 --- tests/test_api_keys.py | 8 +-- tests/test_organization_domains.py | 62 +++++++++---------- tests/test_organizations.py | 8 +-- 19 files changed, 44 insertions(+), 162 deletions(-) diff --git a/src/workos/api_keys/_resource.py b/src/workos/api_keys/_resource.py index c0b71176..68950b58 100644 --- a/src/workos/api_keys/_resource.py +++ b/src/workos/api_keys/_resource.py @@ -51,7 +51,7 @@ def validate_api_key( request_options=request_options, ) - def delete_api_key( + def delete( self, id: str, *, @@ -77,8 +77,6 @@ def delete_api_key( request_options=request_options, ) - delete = delete_api_key - class AsyncApiKeys: """Api Keys API resources (async).""" @@ -120,7 +118,7 @@ async def validate_api_key( request_options=request_options, ) - async def delete_api_key( + async def delete( self, id: str, *, @@ -145,5 +143,3 @@ async def delete_api_key( path=f"api_keys/{id}", request_options=request_options, ) - - delete = delete_api_key diff --git a/src/workos/applications/_resource.py b/src/workos/applications/_resource.py index b205af7c..629efd78 100644 --- a/src/workos/applications/_resource.py +++ b/src/workos/applications/_resource.py @@ -137,8 +137,6 @@ def get( request_options=request_options, ) - find = get - def update( self, id: str, @@ -337,8 +335,6 @@ async def get( request_options=request_options, ) - find = get - async def update( self, id: str, diff --git a/src/workos/audit_logs/_resource.py b/src/workos/audit_logs/_resource.py index b7e5c52e..28900a36 100644 --- a/src/workos/audit_logs/_resource.py +++ b/src/workos/audit_logs/_resource.py @@ -125,8 +125,6 @@ def list_schemas( request_options=request_options, ) - schemas = list_schemas - def create_schema( self, action_name: str, @@ -173,8 +171,6 @@ def create_schema( request_options=request_options, ) - create_schemas = create_schema - def create_event( self, *, @@ -223,8 +219,6 @@ def create_event( request_options=request_options, ) - create_events = create_event - def exports( self, *, @@ -419,8 +413,6 @@ async def list_schemas( request_options=request_options, ) - schemas = list_schemas - async def create_schema( self, action_name: str, @@ -467,8 +459,6 @@ async def create_schema( request_options=request_options, ) - create_schemas = create_schema - async def create_event( self, *, @@ -517,8 +507,6 @@ async def create_event( request_options=request_options, ) - create_events = create_event - async def exports( self, *, diff --git a/src/workos/authorization/_resource.py b/src/workos/authorization/_resource.py index 16f05b4b..d57769e2 100644 --- a/src/workos/authorization/_resource.py +++ b/src/workos/authorization/_resource.py @@ -292,8 +292,6 @@ def remove_role( request_options=request_options, ) - remove_role_by_criteria = remove_role - def remove_role_by_id( self, organization_membership_id: str, @@ -437,8 +435,6 @@ def get_roles_organization( request_options=request_options, ) - get_roles_organizations = get_roles_organization - def update_roles_organizations( self, organization_id: str, @@ -959,8 +955,6 @@ def create_resource( request_options=request_options, ) - create_resources = create_resource - def get_by_id( self, resource_id: str, @@ -993,8 +987,6 @@ def get_by_id( request_options=request_options, ) - find_by_id = get_by_id - def update_resource( self, resource_id: str, @@ -1051,8 +1043,6 @@ def update_resource( request_options=request_options, ) - update_resources = update_resource - def delete_resource( self, resource_id: str, @@ -1092,8 +1082,6 @@ def delete_resource( request_options=request_options, ) - delete_resources = delete_resource - def list_organization_memberships_for_resource( self, resource_id: str, @@ -1655,8 +1643,6 @@ async def remove_role( request_options=request_options, ) - remove_role_by_criteria = remove_role - async def remove_role_by_id( self, organization_membership_id: str, @@ -1800,8 +1786,6 @@ async def get_roles_organization( request_options=request_options, ) - get_roles_organizations = get_roles_organization - async def update_roles_organizations( self, organization_id: str, @@ -2322,8 +2306,6 @@ async def create_resource( request_options=request_options, ) - create_resources = create_resource - async def get_by_id( self, resource_id: str, @@ -2356,8 +2338,6 @@ async def get_by_id( request_options=request_options, ) - find_by_id = get_by_id - async def update_resource( self, resource_id: str, @@ -2414,8 +2394,6 @@ async def update_resource( request_options=request_options, ) - update_resources = update_resource - async def delete_resource( self, resource_id: str, @@ -2455,8 +2433,6 @@ async def delete_resource( request_options=request_options, ) - delete_resources = delete_resource - async def list_organization_memberships_for_resource( self, resource_id: str, diff --git a/src/workos/connections/_resource.py b/src/workos/connections/_resource.py index 5fec78a4..ca6405b0 100644 --- a/src/workos/connections/_resource.py +++ b/src/workos/connections/_resource.py @@ -110,8 +110,6 @@ def get( request_options=request_options, ) - find = get - def delete( self, id: str, @@ -237,8 +235,6 @@ async def get( request_options=request_options, ) - find = get - async def delete( self, id: str, diff --git a/src/workos/directories/_resource.py b/src/workos/directories/_resource.py index 01350170..764e05ab 100644 --- a/src/workos/directories/_resource.py +++ b/src/workos/directories/_resource.py @@ -107,8 +107,6 @@ def get( request_options=request_options, ) - find = get - def delete( self, id: str, @@ -135,8 +133,6 @@ def delete( request_options=request_options, ) - delete_directory = delete - class AsyncDirectories: """Directories API resources (async).""" @@ -232,8 +228,6 @@ async def get( request_options=request_options, ) - find = get - async def delete( self, id: str, @@ -259,5 +253,3 @@ async def delete( path=f"directories/{id}", request_options=request_options, ) - - delete_directory = delete diff --git a/src/workos/directory_groups/_resource.py b/src/workos/directory_groups/_resource.py index 66cc6003..ec891fd8 100644 --- a/src/workos/directory_groups/_resource.py +++ b/src/workos/directory_groups/_resource.py @@ -105,8 +105,6 @@ def get( request_options=request_options, ) - find = get - class AsyncDirectoryGroups: """Directory Groups API resources (async).""" @@ -199,5 +197,3 @@ async def get( model=DirectoryGroup, request_options=request_options, ) - - find = get diff --git a/src/workos/directory_users/_resource.py b/src/workos/directory_users/_resource.py index f97fea5b..1efc7da5 100644 --- a/src/workos/directory_users/_resource.py +++ b/src/workos/directory_users/_resource.py @@ -105,8 +105,6 @@ def get( request_options=request_options, ) - find = get - class AsyncDirectoryUsers: """Directory Users API resources (async).""" @@ -199,5 +197,3 @@ async def get( model=DirectoryUserWithGroups, request_options=request_options, ) - - find = get diff --git a/src/workos/events/_resource.py b/src/workos/events/_resource.py index 1d7203b2..04384e15 100644 --- a/src/workos/events/_resource.py +++ b/src/workos/events/_resource.py @@ -79,8 +79,6 @@ def list_events( request_options=request_options, ) - list = list_events - class AsyncEvents: """Events API resources (async).""" @@ -147,5 +145,3 @@ async def list_events( params=params, request_options=request_options, ) - - list = list_events diff --git a/src/workos/feature_flags/_resource.py b/src/workos/feature_flags/_resource.py index 589ee571..8c00d9b9 100644 --- a/src/workos/feature_flags/_resource.py +++ b/src/workos/feature_flags/_resource.py @@ -98,8 +98,6 @@ def get_by_slug( request_options=request_options, ) - find_by_slug = get_by_slug - def disable_flag( self, slug: str, @@ -246,8 +244,6 @@ async def get_by_slug( request_options=request_options, ) - find_by_slug = get_by_slug - async def disable_flag( self, slug: str, diff --git a/src/workos/organization_domains/_resource.py b/src/workos/organization_domains/_resource.py index 09ca3c48..699dfc7b 100644 --- a/src/workos/organization_domains/_resource.py +++ b/src/workos/organization_domains/_resource.py @@ -17,7 +17,7 @@ class OrganizationDomains: def __init__(self, client: "WorkOSClient") -> None: self._client = client - def create_organization_domain( + def create( self, *, domain: str, @@ -54,9 +54,7 @@ def create_organization_domain( request_options=request_options, ) - create = create_organization_domain - - def get_organization_domain( + def get( self, id: str, *, @@ -86,9 +84,7 @@ def get_organization_domain( request_options=request_options, ) - get = get_organization_domain - - def delete_organization_domain( + def delete( self, id: str, *, @@ -114,8 +110,6 @@ def delete_organization_domain( request_options=request_options, ) - delete = delete_organization_domain - def verify_organization_domain( self, id: str, @@ -146,8 +140,6 @@ def verify_organization_domain( request_options=request_options, ) - verify = verify_organization_domain - class AsyncOrganizationDomains: """Organization Domains API resources (async).""" @@ -155,7 +147,7 @@ class AsyncOrganizationDomains: def __init__(self, client: "AsyncWorkOSClient") -> None: self._client = client - async def create_organization_domain( + async def create( self, *, domain: str, @@ -192,9 +184,7 @@ async def create_organization_domain( request_options=request_options, ) - create = create_organization_domain - - async def get_organization_domain( + async def get( self, id: str, *, @@ -224,9 +214,7 @@ async def get_organization_domain( request_options=request_options, ) - get = get_organization_domain - - async def delete_organization_domain( + async def delete( self, id: str, *, @@ -252,8 +240,6 @@ async def delete_organization_domain( request_options=request_options, ) - delete = delete_organization_domain - async def verify_organization_domain( self, id: str, @@ -283,5 +269,3 @@ async def verify_organization_domain( model=OrganizationDomainStandAlone, request_options=request_options, ) - - verify = verify_organization_domain diff --git a/src/workos/organizations/_resource.py b/src/workos/organizations/_resource.py index 1f9e3bc7..63fcfede 100644 --- a/src/workos/organizations/_resource.py +++ b/src/workos/organizations/_resource.py @@ -194,8 +194,6 @@ def get( request_options=request_options, ) - find = get - def update( self, id: str, @@ -560,8 +558,6 @@ async def get( request_options=request_options, ) - find = get - async def update( self, id: str, diff --git a/src/workos/permissions/_resource.py b/src/workos/permissions/_resource.py index 847fe551..dca1a0cf 100644 --- a/src/workos/permissions/_resource.py +++ b/src/workos/permissions/_resource.py @@ -146,8 +146,6 @@ def get( request_options=request_options, ) - find = get - def update( self, slug: str, @@ -354,8 +352,6 @@ async def get( request_options=request_options, ) - find = get - async def update( self, slug: str, diff --git a/src/workos/sso/_resource.py b/src/workos/sso/_resource.py index 3cb2fa4f..ff1d34ef 100644 --- a/src/workos/sso/_resource.py +++ b/src/workos/sso/_resource.py @@ -245,8 +245,6 @@ def get_profile_and_token( request_options=request_options, ) - token = get_profile_and_token - # @oagen-ignore-start def list_connections( self, @@ -560,8 +558,6 @@ async def get_profile_and_token( request_options=request_options, ) - token = get_profile_and_token - # @oagen-ignore-start async def list_connections( self, diff --git a/src/workos/user_management/data_providers/_resource.py b/src/workos/user_management/data_providers/_resource.py index 789a11eb..b3f92c6a 100644 --- a/src/workos/user_management/data_providers/_resource.py +++ b/src/workos/user_management/data_providers/_resource.py @@ -137,8 +137,6 @@ def get_user_data_integration( request_options=request_options, ) - get_user_data_integrations = get_user_data_integration - class AsyncUserManagementDataProviders: """User Management Data Providers API resources (async).""" @@ -265,5 +263,3 @@ async def get_user_data_integration( model=DataIntegrationsListResponse, request_options=request_options, ) - - get_user_data_integrations = get_user_data_integration diff --git a/src/workos/user_management/users/_resource.py b/src/workos/user_management/users/_resource.py index c5e2c610..6775b337 100644 --- a/src/workos/user_management/users/_resource.py +++ b/src/workos/user_management/users/_resource.py @@ -348,8 +348,6 @@ def get_user( request_options=request_options, ) - get_users = get_user - def update( self, id: str, @@ -546,8 +544,6 @@ def get_identity( for item in (raw if isinstance(raw, list) else []) ] - get_identities = get_identity - def list_sessions( self, id: str, @@ -921,8 +917,6 @@ async def get_user( request_options=request_options, ) - get_users = get_user - async def update( self, id: str, @@ -1119,8 +1113,6 @@ async def get_identity( for item in (raw if isinstance(raw, list) else []) ] - get_identities = get_identity - async def list_sessions( self, id: str, diff --git a/tests/test_api_keys.py b/tests/test_api_keys.py index 0d03277b..7bd997f6 100644 --- a/tests/test_api_keys.py +++ b/tests/test_api_keys.py @@ -28,9 +28,9 @@ def test_validate_api_key(self, workos, httpx_mock): body = json.loads(request.content) assert body["value"] == "test_value" - def test_delete_api_key(self, workos, httpx_mock): + def test_delete(self, workos, httpx_mock): httpx_mock.add_response(status_code=204) - result = workos.api_keys.delete_api_key("test_id") + result = workos.api_keys.delete("test_id") assert result is None request = httpx_mock.get_request() assert request.method == "DELETE" @@ -86,9 +86,9 @@ async def test_validate_api_key(self, async_workos, httpx_mock): assert request.method == "POST" assert request.url.path.endswith("/api_keys/validations") - async def test_delete_api_key(self, async_workos, httpx_mock): + async def test_delete(self, async_workos, httpx_mock): httpx_mock.add_response(status_code=204) - result = await async_workos.api_keys.delete_api_key("test_id") + result = await async_workos.api_keys.delete("test_id") assert result is None request = httpx_mock.get_request() assert request.method == "DELETE" diff --git a/tests/test_organization_domains.py b/tests/test_organization_domains.py index 5556e063..352babab 100644 --- a/tests/test_organization_domains.py +++ b/tests/test_organization_domains.py @@ -19,11 +19,11 @@ class TestOrganizationDomains: - def test_create_organization_domain(self, workos, httpx_mock): + def test_create(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("organization_domain.json"), ) - result = workos.organization_domains.create_organization_domain( + result = workos.organization_domains.create( domain="test_domain", organization_id="test_organization_id" ) assert isinstance(result, OrganizationDomain) @@ -36,11 +36,11 @@ def test_create_organization_domain(self, workos, httpx_mock): assert body["domain"] == "test_domain" assert body["organization_id"] == "test_organization_id" - def test_get_organization_domain(self, workos, httpx_mock): + def test_get(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("organization_domain_stand_alone.json"), ) - result = workos.organization_domains.get_organization_domain("test_id") + result = workos.organization_domains.get("test_id") assert isinstance(result, OrganizationDomainStandAlone) assert result.object == "organization_domain" assert result.id == "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A" @@ -48,9 +48,9 @@ def test_get_organization_domain(self, workos, httpx_mock): assert request.method == "GET" assert request.url.path.endswith("/organization_domains/test_id") - def test_delete_organization_domain(self, workos, httpx_mock): + def test_delete(self, workos, httpx_mock): httpx_mock.add_response(status_code=204) - result = workos.organization_domains.delete_organization_domain("test_id") + result = workos.organization_domains.delete("test_id") assert result is None request = httpx_mock.get_request() assert request.method == "DELETE" @@ -68,28 +68,28 @@ def test_verify_organization_domain(self, workos, httpx_mock): assert request.method == "POST" assert request.url.path.endswith("/organization_domains/test_id/verify") - def test_create_organization_domain_unauthorized(self, workos, httpx_mock): + def test_create_unauthorized(self, workos, httpx_mock): httpx_mock.add_response( status_code=401, json={"message": "Unauthorized"}, ) with pytest.raises(AuthenticationException): - workos.organization_domains.create_organization_domain( + workos.organization_domains.create( domain="test_domain", organization_id="test_organization_id" ) - def test_create_organization_domain_not_found(self, httpx_mock): + def test_create_not_found(self, httpx_mock): workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) try: httpx_mock.add_response(status_code=404, json={"message": "Not found"}) with pytest.raises(NotFoundException): - workos.organization_domains.create_organization_domain( + workos.organization_domains.create( domain="test_domain", organization_id="test_organization_id" ) finally: workos.close() - def test_create_organization_domain_rate_limited(self, httpx_mock): + def test_create_rate_limited(self, httpx_mock): workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) try: httpx_mock.add_response( @@ -98,18 +98,18 @@ def test_create_organization_domain_rate_limited(self, httpx_mock): json={"message": "Slow down"}, ) with pytest.raises(RateLimitExceededException): - workos.organization_domains.create_organization_domain( + workos.organization_domains.create( domain="test_domain", organization_id="test_organization_id" ) finally: workos.close() - def test_create_organization_domain_server_error(self, httpx_mock): + def test_create_server_error(self, httpx_mock): workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) try: httpx_mock.add_response(status_code=500, json={"message": "Server error"}) with pytest.raises(ServerException): - workos.organization_domains.create_organization_domain( + workos.organization_domains.create( domain="test_domain", organization_id="test_organization_id" ) finally: @@ -118,9 +118,9 @@ def test_create_organization_domain_server_error(self, httpx_mock): @pytest.mark.asyncio class TestAsyncOrganizationDomains: - async def test_create_organization_domain(self, async_workos, httpx_mock): + async def test_create(self, async_workos, httpx_mock): httpx_mock.add_response(json=load_fixture("organization_domain.json")) - result = await async_workos.organization_domains.create_organization_domain( + result = await async_workos.organization_domains.create( domain="test_domain", organization_id="test_organization_id" ) assert isinstance(result, OrganizationDomain) @@ -130,13 +130,11 @@ async def test_create_organization_domain(self, async_workos, httpx_mock): assert request.method == "POST" assert request.url.path.endswith("/organization_domains") - async def test_get_organization_domain(self, async_workos, httpx_mock): + async def test_get(self, async_workos, httpx_mock): httpx_mock.add_response( json=load_fixture("organization_domain_stand_alone.json") ) - result = await async_workos.organization_domains.get_organization_domain( - "test_id" - ) + result = await async_workos.organization_domains.get("test_id") assert isinstance(result, OrganizationDomainStandAlone) assert result.object == "organization_domain" assert result.id == "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A" @@ -144,11 +142,9 @@ async def test_get_organization_domain(self, async_workos, httpx_mock): assert request.method == "GET" assert request.url.path.endswith("/organization_domains/test_id") - async def test_delete_organization_domain(self, async_workos, httpx_mock): + async def test_delete(self, async_workos, httpx_mock): httpx_mock.add_response(status_code=204) - result = await async_workos.organization_domains.delete_organization_domain( - "test_id" - ) + result = await async_workos.organization_domains.delete("test_id") assert result is None request = httpx_mock.get_request() assert request.method == "DELETE" @@ -168,29 +164,27 @@ async def test_verify_organization_domain(self, async_workos, httpx_mock): assert request.method == "POST" assert request.url.path.endswith("/organization_domains/test_id/verify") - async def test_create_organization_domain_unauthorized( - self, async_workos, httpx_mock - ): + async def test_create_unauthorized(self, async_workos, httpx_mock): httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) with pytest.raises(AuthenticationException): - await async_workos.organization_domains.create_organization_domain( + await async_workos.organization_domains.create( domain="test_domain", organization_id="test_organization_id" ) - async def test_create_organization_domain_not_found(self, httpx_mock): + async def test_create_not_found(self, httpx_mock): workos = AsyncWorkOS( api_key="sk_test_123", client_id="client_test", max_retries=0 ) try: httpx_mock.add_response(status_code=404, json={"message": "Not found"}) with pytest.raises(NotFoundException): - await workos.organization_domains.create_organization_domain( + await workos.organization_domains.create( domain="test_domain", organization_id="test_organization_id" ) finally: await workos.close() - async def test_create_organization_domain_rate_limited(self, httpx_mock): + async def test_create_rate_limited(self, httpx_mock): workos = AsyncWorkOS( api_key="sk_test_123", client_id="client_test", max_retries=0 ) @@ -201,20 +195,20 @@ async def test_create_organization_domain_rate_limited(self, httpx_mock): json={"message": "Slow down"}, ) with pytest.raises(RateLimitExceededException): - await workos.organization_domains.create_organization_domain( + await workos.organization_domains.create( domain="test_domain", organization_id="test_organization_id" ) finally: await workos.close() - async def test_create_organization_domain_server_error(self, httpx_mock): + async def test_create_server_error(self, httpx_mock): workos = AsyncWorkOS( api_key="sk_test_123", client_id="client_test", max_retries=0 ) try: httpx_mock.add_response(status_code=500, json={"message": "Server error"}) with pytest.raises(ServerException): - await workos.organization_domains.create_organization_domain( + await workos.organization_domains.create( domain="test_domain", organization_id="test_organization_id" ) finally: diff --git a/tests/test_organizations.py b/tests/test_organizations.py index 9f59d24c..ba558640 100644 --- a/tests/test_organizations.py +++ b/tests/test_organizations.py @@ -90,7 +90,7 @@ def test_get(self, workos, httpx_mock): assert request.method == "GET" assert request.url.path.endswith("/organizations/test_id") - def test_update_organization(self, workos, httpx_mock): + def test_update(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("organization.json"), ) @@ -102,7 +102,7 @@ def test_update_organization(self, workos, httpx_mock): assert request.method == "PUT" assert request.url.path.endswith("/organizations/test_id") - def test_delete_organization(self, workos, httpx_mock): + def test_delete(self, workos, httpx_mock): httpx_mock.add_response(status_code=204) result = workos.organizations.delete("test_id") assert result is None @@ -250,7 +250,7 @@ async def test_get(self, async_workos, httpx_mock): assert request.method == "GET" assert request.url.path.endswith("/organizations/test_id") - async def test_update_organization(self, async_workos, httpx_mock): + async def test_update(self, async_workos, httpx_mock): httpx_mock.add_response(json=load_fixture("organization.json")) result = await async_workos.organizations.update("test_id") assert isinstance(result, Organization) @@ -260,7 +260,7 @@ async def test_update_organization(self, async_workos, httpx_mock): assert request.method == "PUT" assert request.url.path.endswith("/organizations/test_id") - async def test_delete_organization(self, async_workos, httpx_mock): + async def test_delete(self, async_workos, httpx_mock): httpx_mock.add_response(status_code=204) result = await async_workos.organizations.delete("test_id") assert result is None From b6de8fcc9d3c50d4ef98499c94481c9301ee34f7 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 31 Mar 2026 14:46:51 -0400 Subject: [PATCH 17/26] drop compat layers --- src/workos/__init__.py | 9 +- src/workos/_client.py | 509 +----------------- src/workos/_pagination.py | 4 - src/workos/async_client.py | 7 - src/workos/client.py | 8 - src/workos/exceptions.py | 3 - src/workos/types/__init__.py | 2 - src/workos/types/admin_portal/__init__.py | 3 - src/workos/types/api_keys/__init__.py | 3 - .../application_client_secrets/__init__.py | 3 - src/workos/types/applications/__init__.py | 3 - src/workos/types/audit_logs/__init__.py | 3 - src/workos/types/authorization/__init__.py | 3 - src/workos/types/connect/__init__.py | 3 - src/workos/types/connections/__init__.py | 3 - src/workos/types/directories/__init__.py | 3 - src/workos/types/directory_groups/__init__.py | 3 - src/workos/types/directory_sync/__init__.py | 3 - src/workos/types/directory_users/__init__.py | 3 - src/workos/types/events/__init__.py | 3 - src/workos/types/feature_flags/__init__.py | 4 - .../types/feature_flags/feature_flag.py | 3 - .../types/feature_flags/feature_flag_owner.py | 3 - src/workos/types/feature_flags/flag.py | 3 - src/workos/types/feature_flags/flag_owner.py | 3 - .../types/feature_flags_targets/__init__.py | 3 - src/workos/types/fga/__init__.py | 3 - src/workos/types/mfa/__init__.py | 3 - .../types/multi_factor_auth/__init__.py | 4 - .../authentication_challenge.py | 5 - ...uthentication_challenge_verify_response.py | 5 - ...uthentication_challenges_verify_request.py | 5 - .../authentication_factor.py | 3 - .../authentication_factor_enrolled.py | 5 - .../authentication_factor_enrolled_sms.py | 5 - .../authentication_factor_enrolled_totp.py | 5 - .../authentication_factor_sms.py | 5 - .../authentication_factor_totp.py | 5 - .../authentication_factors_create_request.py | 5 - .../challenge_authentication_factor.py | 5 - .../multi_factor_auth_challenges/__init__.py | 3 - .../types/organization_domains/__init__.py | 3 - src/workos/types/organizations/__init__.py | 5 - .../types/organizations/api_key_with_value.py | 3 - .../organizations/api_key_with_value_owner.py | 5 - .../organizations/audit_log_configuration.py | 3 - .../audit_log_configuration_log_stream.py | 5 - .../audit_logs_retention_json.py | 3 - .../create_organization_api_key.py | 5 - .../types/organizations/organization.py | 3 - .../organizations/organization_domain_data.py | 3 - .../types/organizations/organization_dto.py | 3 - .../update_audit_logs_retention.py | 5 - .../organizations/update_organization.py | 3 - .../types/organizations_api_keys/__init__.py | 3 - .../organizations_feature_flags/__init__.py | 3 - src/workos/types/permissions/__init__.py | 3 - src/workos/types/pipes/__init__.py | 3 - src/workos/types/portal/__init__.py | 3 - src/workos/types/radar/__init__.py | 3 - src/workos/types/sso/__init__.py | 3 - src/workos/types/user_management/__init__.py | 13 - .../user_management/authenticate_response.py | 5 - .../authenticate_response_impersonator.py | 5 - .../authenticate_response_oauth_token.py | 5 - ...ation_code_session_authenticate_request.py | 5 - .../user_management/connected_account.py | 5 - .../user_management/cors_origin_response.py | 5 - .../user_management/create_cors_origin.py | 5 - .../create_magic_code_and_return.py | 5 - .../user_management/create_password_reset.py | 5 - .../create_password_reset_token.py | 5 - .../user_management/create_redirect_uri.py | 5 - .../types/user_management/create_user.py | 3 - .../create_user_invite_options.py | 5 - .../create_user_organization_membership.py | 5 - .../data_integrations_list_response.py | 5 - .../data_integrations_list_response_data.py | 5 - ...ns_list_response_data_connected_account.py | 5 - .../device_authorization_response.py | 5 - .../user_management/email_verification.py | 3 - .../enroll_user_authentication_factor.py | 5 - .../types/user_management/invitation.py | 3 - .../types/user_management/jwks_response.py | 3 - .../user_management/jwks_response_keys.py | 5 - .../user_management/jwt_template_response.py | 5 - .../types/user_management/magic_auth.py | 3 - .../organization_membership.py | 5 - .../types/user_management/password_reset.py | 3 - .../password_session_authenticate_request.py | 5 - .../types/user_management/redirect_uri.py | 3 - ...resh_token_session_authenticate_request.py | 5 - .../resend_user_invite_options.py | 5 - .../reset_password_response.py | 5 - .../types/user_management/revoke_session.py | 3 - .../send_verification_email_response.py | 5 - .../sso_device_authorization_request.py | 5 - .../user_management/update_jwt_template.py | 5 - .../types/user_management/update_user.py | 3 - .../update_user_organization_membership.py | 5 - ...evice_code_session_authenticate_request.py | 5 - ...ation_code_session_authenticate_request.py | 5 - ..._auth_code_session_authenticate_request.py | 5 - ...e_mfa_totp_session_authenticate_request.py | 5 - ..._selection_session_authenticate_request.py | 5 - src/workos/types/user_management/user.py | 3 - ...r_authentication_factor_enroll_response.py | 5 - .../user_identities_get_item.py | 5 - .../types/user_management/user_invite.py | 3 - .../user_organization_membership.py | 5 - .../user_sessions_impersonator.py | 5 - .../user_sessions_list_item.py | 5 - .../user_management/verify_email_address.py | 3 - .../user_management/verify_email_response.py | 5 - .../__init__.py | 3 - .../user_management_cors_origins/__init__.py | 3 - .../__init__.py | 3 - .../user_management_invitations/__init__.py | 3 - .../user_management_jwt_template/__init__.py | 3 - .../user_management_magic_auth/__init__.py | 3 - .../__init__.py | 3 - .../__init__.py | 3 - .../user_management_redirect_uris/__init__.py | 3 - .../__init__.py | 3 - .../types/user_management_users/__init__.py | 4 - ...uthorized_connect_application_list_data.py | 5 - .../__init__.py | 3 - .../__init__.py | 3 - src/workos/types/webhooks/__init__.py | 3 - src/workos/types/widgets/__init__.py | 3 - src/workos/types/workos_connect/__init__.py | 3 - 131 files changed, 2 insertions(+), 1037 deletions(-) delete mode 100644 src/workos/async_client.py delete mode 100644 src/workos/client.py delete mode 100644 src/workos/exceptions.py delete mode 100644 src/workos/types/__init__.py delete mode 100644 src/workos/types/admin_portal/__init__.py delete mode 100644 src/workos/types/api_keys/__init__.py delete mode 100644 src/workos/types/application_client_secrets/__init__.py delete mode 100644 src/workos/types/applications/__init__.py delete mode 100644 src/workos/types/audit_logs/__init__.py delete mode 100644 src/workos/types/authorization/__init__.py delete mode 100644 src/workos/types/connect/__init__.py delete mode 100644 src/workos/types/connections/__init__.py delete mode 100644 src/workos/types/directories/__init__.py delete mode 100644 src/workos/types/directory_groups/__init__.py delete mode 100644 src/workos/types/directory_sync/__init__.py delete mode 100644 src/workos/types/directory_users/__init__.py delete mode 100644 src/workos/types/events/__init__.py delete mode 100644 src/workos/types/feature_flags/__init__.py delete mode 100644 src/workos/types/feature_flags/feature_flag.py delete mode 100644 src/workos/types/feature_flags/feature_flag_owner.py delete mode 100644 src/workos/types/feature_flags/flag.py delete mode 100644 src/workos/types/feature_flags/flag_owner.py delete mode 100644 src/workos/types/feature_flags_targets/__init__.py delete mode 100644 src/workos/types/fga/__init__.py delete mode 100644 src/workos/types/mfa/__init__.py delete mode 100644 src/workos/types/multi_factor_auth/__init__.py delete mode 100644 src/workos/types/multi_factor_auth/authentication_challenge.py delete mode 100644 src/workos/types/multi_factor_auth/authentication_challenge_verify_response.py delete mode 100644 src/workos/types/multi_factor_auth/authentication_challenges_verify_request.py delete mode 100644 src/workos/types/multi_factor_auth/authentication_factor.py delete mode 100644 src/workos/types/multi_factor_auth/authentication_factor_enrolled.py delete mode 100644 src/workos/types/multi_factor_auth/authentication_factor_enrolled_sms.py delete mode 100644 src/workos/types/multi_factor_auth/authentication_factor_enrolled_totp.py delete mode 100644 src/workos/types/multi_factor_auth/authentication_factor_sms.py delete mode 100644 src/workos/types/multi_factor_auth/authentication_factor_totp.py delete mode 100644 src/workos/types/multi_factor_auth/authentication_factors_create_request.py delete mode 100644 src/workos/types/multi_factor_auth/challenge_authentication_factor.py delete mode 100644 src/workos/types/multi_factor_auth_challenges/__init__.py delete mode 100644 src/workos/types/organization_domains/__init__.py delete mode 100644 src/workos/types/organizations/__init__.py delete mode 100644 src/workos/types/organizations/api_key_with_value.py delete mode 100644 src/workos/types/organizations/api_key_with_value_owner.py delete mode 100644 src/workos/types/organizations/audit_log_configuration.py delete mode 100644 src/workos/types/organizations/audit_log_configuration_log_stream.py delete mode 100644 src/workos/types/organizations/audit_logs_retention_json.py delete mode 100644 src/workos/types/organizations/create_organization_api_key.py delete mode 100644 src/workos/types/organizations/organization.py delete mode 100644 src/workos/types/organizations/organization_domain_data.py delete mode 100644 src/workos/types/organizations/organization_dto.py delete mode 100644 src/workos/types/organizations/update_audit_logs_retention.py delete mode 100644 src/workos/types/organizations/update_organization.py delete mode 100644 src/workos/types/organizations_api_keys/__init__.py delete mode 100644 src/workos/types/organizations_feature_flags/__init__.py delete mode 100644 src/workos/types/permissions/__init__.py delete mode 100644 src/workos/types/pipes/__init__.py delete mode 100644 src/workos/types/portal/__init__.py delete mode 100644 src/workos/types/radar/__init__.py delete mode 100644 src/workos/types/sso/__init__.py delete mode 100644 src/workos/types/user_management/__init__.py delete mode 100644 src/workos/types/user_management/authenticate_response.py delete mode 100644 src/workos/types/user_management/authenticate_response_impersonator.py delete mode 100644 src/workos/types/user_management/authenticate_response_oauth_token.py delete mode 100644 src/workos/types/user_management/authorization_code_session_authenticate_request.py delete mode 100644 src/workos/types/user_management/connected_account.py delete mode 100644 src/workos/types/user_management/cors_origin_response.py delete mode 100644 src/workos/types/user_management/create_cors_origin.py delete mode 100644 src/workos/types/user_management/create_magic_code_and_return.py delete mode 100644 src/workos/types/user_management/create_password_reset.py delete mode 100644 src/workos/types/user_management/create_password_reset_token.py delete mode 100644 src/workos/types/user_management/create_redirect_uri.py delete mode 100644 src/workos/types/user_management/create_user.py delete mode 100644 src/workos/types/user_management/create_user_invite_options.py delete mode 100644 src/workos/types/user_management/create_user_organization_membership.py delete mode 100644 src/workos/types/user_management/data_integrations_list_response.py delete mode 100644 src/workos/types/user_management/data_integrations_list_response_data.py delete mode 100644 src/workos/types/user_management/data_integrations_list_response_data_connected_account.py delete mode 100644 src/workos/types/user_management/device_authorization_response.py delete mode 100644 src/workos/types/user_management/email_verification.py delete mode 100644 src/workos/types/user_management/enroll_user_authentication_factor.py delete mode 100644 src/workos/types/user_management/invitation.py delete mode 100644 src/workos/types/user_management/jwks_response.py delete mode 100644 src/workos/types/user_management/jwks_response_keys.py delete mode 100644 src/workos/types/user_management/jwt_template_response.py delete mode 100644 src/workos/types/user_management/magic_auth.py delete mode 100644 src/workos/types/user_management/organization_membership.py delete mode 100644 src/workos/types/user_management/password_reset.py delete mode 100644 src/workos/types/user_management/password_session_authenticate_request.py delete mode 100644 src/workos/types/user_management/redirect_uri.py delete mode 100644 src/workos/types/user_management/refresh_token_session_authenticate_request.py delete mode 100644 src/workos/types/user_management/resend_user_invite_options.py delete mode 100644 src/workos/types/user_management/reset_password_response.py delete mode 100644 src/workos/types/user_management/revoke_session.py delete mode 100644 src/workos/types/user_management/send_verification_email_response.py delete mode 100644 src/workos/types/user_management/sso_device_authorization_request.py delete mode 100644 src/workos/types/user_management/update_jwt_template.py delete mode 100644 src/workos/types/user_management/update_user.py delete mode 100644 src/workos/types/user_management/update_user_organization_membership.py delete mode 100644 src/workos/types/user_management/urn_ietf_params_oauth_grant_type_device_code_session_authenticate_request.py delete mode 100644 src/workos/types/user_management/urn_workos_oauth_grant_type_email_verification_code_session_authenticate_request.py delete mode 100644 src/workos/types/user_management/urn_workos_oauth_grant_type_magic_auth_code_session_authenticate_request.py delete mode 100644 src/workos/types/user_management/urn_workos_oauth_grant_type_mfa_totp_session_authenticate_request.py delete mode 100644 src/workos/types/user_management/urn_workos_oauth_grant_type_organization_selection_session_authenticate_request.py delete mode 100644 src/workos/types/user_management/user.py delete mode 100644 src/workos/types/user_management/user_authentication_factor_enroll_response.py delete mode 100644 src/workos/types/user_management/user_identities_get_item.py delete mode 100644 src/workos/types/user_management/user_invite.py delete mode 100644 src/workos/types/user_management/user_organization_membership.py delete mode 100644 src/workos/types/user_management/user_sessions_impersonator.py delete mode 100644 src/workos/types/user_management/user_sessions_list_item.py delete mode 100644 src/workos/types/user_management/verify_email_address.py delete mode 100644 src/workos/types/user_management/verify_email_response.py delete mode 100644 src/workos/types/user_management_authentication/__init__.py delete mode 100644 src/workos/types/user_management_cors_origins/__init__.py delete mode 100644 src/workos/types/user_management_data_providers/__init__.py delete mode 100644 src/workos/types/user_management_invitations/__init__.py delete mode 100644 src/workos/types/user_management_jwt_template/__init__.py delete mode 100644 src/workos/types/user_management_magic_auth/__init__.py delete mode 100644 src/workos/types/user_management_multi_factor_authentication/__init__.py delete mode 100644 src/workos/types/user_management_organization_membership/__init__.py delete mode 100644 src/workos/types/user_management_redirect_uris/__init__.py delete mode 100644 src/workos/types/user_management_session_tokens/__init__.py delete mode 100644 src/workos/types/user_management_users/__init__.py delete mode 100644 src/workos/types/user_management_users/authorized_connect_application_list_data.py delete mode 100644 src/workos/types/user_management_users_authorized_applications/__init__.py delete mode 100644 src/workos/types/user_management_users_feature_flags/__init__.py delete mode 100644 src/workos/types/webhooks/__init__.py delete mode 100644 src/workos/types/widgets/__init__.py delete mode 100644 src/workos/types/workos_connect/__init__.py diff --git a/src/workos/__init__.py b/src/workos/__init__.py index f1994616..0963f2d5 100644 --- a/src/workos/__init__.py +++ b/src/workos/__init__.py @@ -17,18 +17,12 @@ WorkOSConnectionException, WorkOSTimeoutException, ) -from ._pagination import AsyncPage, SyncPage, WorkOSListResource +from ._pagination import AsyncPage, SyncPage from ._types import RequestOptions -# Backward-compatible aliases -WorkOS = WorkOSClient -AsyncWorkOS = AsyncWorkOSClient - __all__ = [ "WorkOSClient", "AsyncWorkOSClient", - "WorkOS", - "AsyncWorkOS", "RequestOptions", "BaseRequestException", "AuthenticationException", @@ -44,5 +38,4 @@ "WorkOSTimeoutException", "AsyncPage", "SyncPage", - "WorkOSListResource", ] diff --git a/src/workos/_client.py b/src/workos/_client.py index 14ea0fda..bc491ddb 100644 --- a/src/workos/_client.py +++ b/src/workos/_client.py @@ -11,7 +11,7 @@ import random from datetime import datetime, timezone from email.utils import parsedate_to_datetime -from typing import Any, Dict, List, Literal, Optional, Type, cast, overload +from typing import Any, Dict, Optional, Type, cast, overload import httpx @@ -121,16 +121,6 @@ from .webhooks._resource import Webhooks, AsyncWebhooks from .widgets._resource import Widgets, AsyncWidgets from .audit_logs._resource import AuditLogs, AsyncAuditLogs -from .session import AsyncSession, Session -from .directory_users.models import DirectoryUsersOrder -from .directory_groups.models import DirectoryGroupsOrder -from .directories.models import DirectoriesOrder -from .user_management.users.models import UserManagementUsersOrder -from .user_management.authentication.models import ( - UserManagementAuthenticationProvider, - UserManagementAuthenticationScreenHint, -) -from .common.models import CreateUserDtoPasswordHashType, UpdateUserDtoPasswordHashType try: from importlib.metadata import version as _pkg_version @@ -233,140 +223,6 @@ def data_providers(self) -> UserManagementDataProviders: def multi_factor_authentication(self) -> UserManagementMultiFactorAuthentication: return UserManagementMultiFactorAuthentication(self._client) - def load_sealed_session( - self, *, sealed_session: str, cookie_password: str - ) -> Session: - return Session( - client=self._client, - session_data=sealed_session, - cookie_password=cookie_password, - ) - - def get_user(self, user_id: str): - return self.users.get_user(user_id) - - def get_user_by_external_id(self, external_id: str): - return self.users.get_by_external_id(external_id) - - def list_users( - self, - *, - email: Optional[str] = None, - organization_id: Optional[str] = None, - limit: Optional[int] = None, - before: Optional[str] = None, - after: Optional[str] = None, - order: Optional[UserManagementUsersOrder] = None, - ): - return self.users.list_users( - email=email, - organization_id=organization_id, - limit=limit, - before=before, - after=after, - order=order, - ) - - def create_user( - self, - *, - email: str, - password: Optional[str] = None, - password_hash: Optional[str] = None, - password_hash_type: Optional[CreateUserDtoPasswordHashType] = None, - first_name: Optional[str] = None, - last_name: Optional[str] = None, - email_verified: Optional[bool] = None, - external_id: Optional[str] = None, - metadata: Optional[Dict[str, str]] = None, - ): - return self.users.create( - email=email, - password=password, - password_hash=password_hash, - password_hash_type=password_hash_type, - first_name=first_name, - last_name=last_name, - email_verified=email_verified, - external_id=external_id, - metadata=metadata, - ) - - def update_user( - self, - *, - user_id: str, - email: Optional[str] = None, - first_name: Optional[str] = None, - last_name: Optional[str] = None, - email_verified: Optional[bool] = None, - password: Optional[str] = None, - password_hash: Optional[str] = None, - password_hash_type: Optional[UpdateUserDtoPasswordHashType] = None, - external_id: Optional[str] = None, - metadata: Optional[Dict[str, str]] = None, - locale: Optional[str] = None, - ): - return self.users.update( - user_id, - email=email, - first_name=first_name, - last_name=last_name, - email_verified=email_verified, - password=password, - password_hash=password_hash, - password_hash_type=password_hash_type, - external_id=external_id, - metadata=metadata, - locale=locale, - ) - - def delete_user(self, user_id: str) -> None: - self.users.delete(user_id) - - def get_authorization_url( - self, - *, - redirect_uri: str, - domain_hint: Optional[str] = None, - login_hint: Optional[str] = None, - state: Optional[str] = None, - provider: Optional[UserManagementAuthenticationProvider] = None, - connection_id: Optional[str] = None, - organization_id: Optional[str] = None, - code_challenge: Optional[str] = None, - code_challenge_method: Optional[Literal["S256"]] = None, - provider_query_params: Optional[Dict[str, str]] = None, - provider_scopes: Optional[List[str]] = None, - invitation_token: Optional[str] = None, - screen_hint: Optional[UserManagementAuthenticationScreenHint] = None, - prompt: Optional[str] = None, - ) -> str: - return self.authentication.authorize( - redirect_uri=redirect_uri, - client_id=self._client.client_id or "", - response_type="code", - domain_hint=domain_hint, - login_hint=login_hint, - state=state, - provider=provider, - connection_id=connection_id, - organization_id=organization_id, - code_challenge=code_challenge, - code_challenge_method=code_challenge_method, - provider_query_params=provider_query_params, - provider_scopes=provider_scopes, - invitation_token=invitation_token, - screen_hint=screen_hint, - prompt=prompt, - ) - - def get_jwks_url(self) -> str: - return self._client.build_url(f"sso/jwks/{self._client.client_id}") - - def get_logout_url(self, session_id: str, return_to: Optional[str] = None) -> str: - return self.authentication.logout(session_id=session_id, return_to=return_to) - class UserManagementUsersNamespace(object): """UserManagementUsers resources.""" @@ -383,84 +239,6 @@ def authorized_applications(self) -> UserManagementUsersAuthorizedApplications: return UserManagementUsersAuthorizedApplications(self._client) -class DirectorySyncNamespace(object): - """Directory Sync compatibility resources.""" - - def __init__(self, client: "WorkOSClient") -> None: - self._client = client - - def list_users( - self, - *, - directory_id: Optional[str] = None, - group_id: Optional[str] = None, - limit: Optional[int] = None, - before: Optional[str] = None, - after: Optional[str] = None, - order: Optional[DirectoryUsersOrder] = None, - ): - return self._client.directory_users.list( - directory=directory_id, - group=group_id, - limit=limit, - before=before, - after=after, - order=order, - ) - - def list_groups( - self, - *, - directory_id: Optional[str] = None, - user_id: Optional[str] = None, - limit: Optional[int] = None, - before: Optional[str] = None, - after: Optional[str] = None, - order: Optional[DirectoryGroupsOrder] = None, - ): - return self._client.directory_groups.list( - directory=directory_id, - user=user_id, - limit=limit, - before=before, - after=after, - order=order, - ) - - def list_directories( - self, - *, - search: Optional[str] = None, - limit: Optional[int] = None, - before: Optional[str] = None, - after: Optional[str] = None, - organization_id: Optional[str] = None, - order: Optional[DirectoriesOrder] = None, - domain: Optional[str] = None, - ): - return self._client.directories.list( - search=search, - limit=limit, - before=before, - after=after, - organization_id=organization_id, - order=order, - domain=domain, - ) - - def get_user(self, user_id: str): - return self._client.directory_users.get(user_id) - - def get_group(self, group_id: str): - return self._client.directory_groups.get(group_id) - - def get_directory(self, directory_id: str): - return self._client.directories.get(directory_id) - - def delete_directory(self, directory_id: str) -> None: - self._client.directories.delete(directory_id) - - class AsyncMultiFactorAuthNamespace(AsyncMultiFactorAuth): """MultiFactorAuth resources (async).""" @@ -550,144 +328,6 @@ def multi_factor_authentication( ) -> AsyncUserManagementMultiFactorAuthentication: return AsyncUserManagementMultiFactorAuthentication(self._client) - def load_sealed_session( - self, *, sealed_session: str, cookie_password: str - ) -> AsyncSession: - return AsyncSession( - client=self._client, - session_data=sealed_session, - cookie_password=cookie_password, - ) - - async def get_user(self, user_id: str): - return await self.users.get_user(user_id) - - async def get_user_by_external_id(self, external_id: str): - return await self.users.get_by_external_id(external_id) - - async def list_users( - self, - *, - email: Optional[str] = None, - organization_id: Optional[str] = None, - limit: Optional[int] = None, - before: Optional[str] = None, - after: Optional[str] = None, - order: Optional[UserManagementUsersOrder] = None, - ): - return await self.users.list_users( - email=email, - organization_id=organization_id, - limit=limit, - before=before, - after=after, - order=order, - ) - - async def create_user( - self, - *, - email: str, - password: Optional[str] = None, - password_hash: Optional[str] = None, - password_hash_type: Optional[CreateUserDtoPasswordHashType] = None, - first_name: Optional[str] = None, - last_name: Optional[str] = None, - email_verified: Optional[bool] = None, - external_id: Optional[str] = None, - metadata: Optional[Dict[str, str]] = None, - ): - return await self.users.create( - email=email, - password=password, - password_hash=password_hash, - password_hash_type=password_hash_type, - first_name=first_name, - last_name=last_name, - email_verified=email_verified, - external_id=external_id, - metadata=metadata, - ) - - async def update_user( - self, - *, - user_id: str, - email: Optional[str] = None, - first_name: Optional[str] = None, - last_name: Optional[str] = None, - email_verified: Optional[bool] = None, - password: Optional[str] = None, - password_hash: Optional[str] = None, - password_hash_type: Optional[UpdateUserDtoPasswordHashType] = None, - external_id: Optional[str] = None, - metadata: Optional[Dict[str, str]] = None, - locale: Optional[str] = None, - ): - return await self.users.update( - user_id, - email=email, - first_name=first_name, - last_name=last_name, - email_verified=email_verified, - password=password, - password_hash=password_hash, - password_hash_type=password_hash_type, - external_id=external_id, - metadata=metadata, - locale=locale, - ) - - async def delete_user(self, user_id: str) -> None: - await self.users.delete(user_id) - - async def get_authorization_url( - self, - *, - redirect_uri: str, - domain_hint: Optional[str] = None, - login_hint: Optional[str] = None, - state: Optional[str] = None, - provider: Optional[UserManagementAuthenticationProvider] = None, - connection_id: Optional[str] = None, - organization_id: Optional[str] = None, - code_challenge: Optional[str] = None, - code_challenge_method: Optional[Literal["S256"]] = None, - provider_query_params: Optional[Dict[str, str]] = None, - provider_scopes: Optional[List[str]] = None, - invitation_token: Optional[str] = None, - screen_hint: Optional[UserManagementAuthenticationScreenHint] = None, - prompt: Optional[str] = None, - ) -> str: - return await self.authentication.authorize( - redirect_uri=redirect_uri, - client_id=self._client.client_id or "", - response_type="code", - domain_hint=domain_hint, - login_hint=login_hint, - state=state, - provider=provider, - connection_id=connection_id, - organization_id=organization_id, - code_challenge=code_challenge, - code_challenge_method=code_challenge_method, - provider_query_params=provider_query_params, - provider_scopes=provider_scopes, - invitation_token=invitation_token, - screen_hint=screen_hint, - prompt=prompt, - ) - - def get_jwks_url(self) -> str: - return self._client.build_url(f"sso/jwks/{self._client.client_id}") - - async def get_logout_url( - self, session_id: str, return_to: Optional[str] = None - ) -> str: - return await self.authentication.logout( - session_id=session_id, return_to=return_to - ) - class AsyncUserManagementUsersNamespace(object): """UserManagementUsers resources (async).""" @@ -704,84 +344,6 @@ def authorized_applications(self) -> AsyncUserManagementUsersAuthorizedApplicati return AsyncUserManagementUsersAuthorizedApplications(self._client) -class AsyncDirectorySyncNamespace(object): - """Directory Sync compatibility resources (async).""" - - def __init__(self, client: "AsyncWorkOSClient") -> None: - self._client = client - - async def list_users( - self, - *, - directory_id: Optional[str] = None, - group_id: Optional[str] = None, - limit: Optional[int] = None, - before: Optional[str] = None, - after: Optional[str] = None, - order: Optional[DirectoryUsersOrder] = None, - ): - return await self._client.directory_users.list( - directory=directory_id, - group=group_id, - limit=limit, - before=before, - after=after, - order=order, - ) - - async def list_groups( - self, - *, - directory_id: Optional[str] = None, - user_id: Optional[str] = None, - limit: Optional[int] = None, - before: Optional[str] = None, - after: Optional[str] = None, - order: Optional[DirectoryGroupsOrder] = None, - ): - return await self._client.directory_groups.list( - directory=directory_id, - user=user_id, - limit=limit, - before=before, - after=after, - order=order, - ) - - async def list_directories( - self, - *, - search: Optional[str] = None, - limit: Optional[int] = None, - before: Optional[str] = None, - after: Optional[str] = None, - organization_id: Optional[str] = None, - order: Optional[DirectoriesOrder] = None, - domain: Optional[str] = None, - ): - return await self._client.directories.list( - search=search, - limit=limit, - before=before, - after=after, - organization_id=organization_id, - order=order, - domain=domain, - ) - - async def get_user(self, user_id: str): - return await self._client.directory_users.get(user_id) - - async def get_group(self, group_id: str): - return await self._client.directory_groups.get(group_id) - - async def get_directory(self, directory_id: str): - return await self._client.directories.get(directory_id) - - async def delete_directory(self, directory_id: str) -> None: - await self._client.directories.delete(directory_id) - - class _BaseWorkOSClient: """Shared WorkOS client implementation.""" @@ -1136,38 +698,6 @@ def user_management(self) -> UserManagementNamespace: def user_management_users(self) -> UserManagementUsersNamespace: return UserManagementUsersNamespace(self) - @functools.cached_property - def directory_sync(self) -> DirectorySyncNamespace: - return DirectorySyncNamespace(self) - - @functools.cached_property - def connect(self) -> Any: - return self.workos_connect - - @functools.cached_property - def fga(self) -> Any: - return self.authorization - - @functools.cached_property - def mfa(self) -> Any: - return self.multi_factor_auth - - @functools.cached_property - def passwordless(self) -> Any: - from .passwordless import Passwordless - - return Passwordless(self) - - @functools.cached_property - def portal(self) -> Any: - return self.admin_portal - - @functools.cached_property - def vault(self) -> Any: - from .vault import Vault - - return Vault(self) - @overload def request( self, @@ -1435,38 +965,6 @@ def user_management(self) -> AsyncUserManagementNamespace: def user_management_users(self) -> AsyncUserManagementUsersNamespace: return AsyncUserManagementUsersNamespace(self) - @functools.cached_property - def directory_sync(self) -> AsyncDirectorySyncNamespace: - return AsyncDirectorySyncNamespace(self) - - @functools.cached_property - def connect(self) -> Any: - return self.workos_connect - - @functools.cached_property - def fga(self) -> Any: - return self.authorization - - @functools.cached_property - def mfa(self) -> Any: - return self.multi_factor_auth - - @functools.cached_property - def passwordless(self) -> Any: - from .passwordless import AsyncPasswordless - - return AsyncPasswordless(self) - - @functools.cached_property - def portal(self) -> Any: - return self.admin_portal - - @functools.cached_property - def vault(self) -> Any: - from .vault import AsyncVault - - return AsyncVault(self) - @overload async def request( self, @@ -1588,8 +1086,3 @@ async def _fetch(*, after: Optional[str] = None) -> AsyncPage[D]: ) return AsyncPage(data=items, list_metadata=list_metadata, _fetch_page=_fetch) - - -# Backward-compatible aliases -WorkOS = WorkOSClient -AsyncWorkOS = AsyncWorkOSClient diff --git a/src/workos/_pagination.py b/src/workos/_pagination.py index 7839eddf..013d004c 100644 --- a/src/workos/_pagination.py +++ b/src/workos/_pagination.py @@ -101,7 +101,3 @@ async def __aiter__(self) -> AsyncIterator[T]: """Iterate through all items across all pages.""" async for item in self.auto_paging_iter(): yield item - - -# Backward-compatible alias (v5.x naming) -WorkOSListResource = SyncPage diff --git a/src/workos/async_client.py b/src/workos/async_client.py deleted file mode 100644 index 64db2170..00000000 --- a/src/workos/async_client.py +++ /dev/null @@ -1,7 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from ._client import AsyncWorkOSClient, AsyncWorkOS - -AsyncClient = AsyncWorkOSClient - -__all__ = ["AsyncClient", "AsyncWorkOSClient", "AsyncWorkOS"] diff --git a/src/workos/client.py b/src/workos/client.py deleted file mode 100644 index 34e026d6..00000000 --- a/src/workos/client.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from ._client import WorkOSClient, WorkOS - -SyncClient = WorkOSClient -Client = WorkOSClient - -__all__ = ["SyncClient", "Client", "WorkOSClient", "WorkOS"] diff --git a/src/workos/exceptions.py b/src/workos/exceptions.py deleted file mode 100644 index 84a4b6d1..00000000 --- a/src/workos/exceptions.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from ._errors import * # noqa: F401,F403 diff --git a/src/workos/types/__init__.py b/src/workos/types/__init__.py deleted file mode 100644 index 33e9c7b7..00000000 --- a/src/workos/types/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - diff --git a/src/workos/types/admin_portal/__init__.py b/src/workos/types/admin_portal/__init__.py deleted file mode 100644 index d183d123..00000000 --- a/src/workos/types/admin_portal/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.admin_portal.models import * # noqa: F401,F403 diff --git a/src/workos/types/api_keys/__init__.py b/src/workos/types/api_keys/__init__.py deleted file mode 100644 index d83f31ba..00000000 --- a/src/workos/types/api_keys/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.api_keys.models import * # noqa: F401,F403 diff --git a/src/workos/types/application_client_secrets/__init__.py b/src/workos/types/application_client_secrets/__init__.py deleted file mode 100644 index cdce13dd..00000000 --- a/src/workos/types/application_client_secrets/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.application_client_secrets.models import * # noqa: F401,F403 diff --git a/src/workos/types/applications/__init__.py b/src/workos/types/applications/__init__.py deleted file mode 100644 index 9183ef05..00000000 --- a/src/workos/types/applications/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.applications.models import * # noqa: F401,F403 diff --git a/src/workos/types/audit_logs/__init__.py b/src/workos/types/audit_logs/__init__.py deleted file mode 100644 index 7a55f4aa..00000000 --- a/src/workos/types/audit_logs/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.audit_logs.models import * # noqa: F401,F403 diff --git a/src/workos/types/authorization/__init__.py b/src/workos/types/authorization/__init__.py deleted file mode 100644 index 599e9b5a..00000000 --- a/src/workos/types/authorization/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.authorization.models import * # noqa: F401,F403 diff --git a/src/workos/types/connect/__init__.py b/src/workos/types/connect/__init__.py deleted file mode 100644 index 96cf742d..00000000 --- a/src/workos/types/connect/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.workos_connect.models import * # noqa: F401,F403 diff --git a/src/workos/types/connections/__init__.py b/src/workos/types/connections/__init__.py deleted file mode 100644 index d8bef48a..00000000 --- a/src/workos/types/connections/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.connections.models import * # noqa: F401,F403 diff --git a/src/workos/types/directories/__init__.py b/src/workos/types/directories/__init__.py deleted file mode 100644 index ffced000..00000000 --- a/src/workos/types/directories/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.directories.models import * # noqa: F401,F403 diff --git a/src/workos/types/directory_groups/__init__.py b/src/workos/types/directory_groups/__init__.py deleted file mode 100644 index c257566b..00000000 --- a/src/workos/types/directory_groups/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.directory_groups.models import * # noqa: F401,F403 diff --git a/src/workos/types/directory_sync/__init__.py b/src/workos/types/directory_sync/__init__.py deleted file mode 100644 index ffced000..00000000 --- a/src/workos/types/directory_sync/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.directories.models import * # noqa: F401,F403 diff --git a/src/workos/types/directory_users/__init__.py b/src/workos/types/directory_users/__init__.py deleted file mode 100644 index 2a5bae9e..00000000 --- a/src/workos/types/directory_users/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.directory_users.models import * # noqa: F401,F403 diff --git a/src/workos/types/events/__init__.py b/src/workos/types/events/__init__.py deleted file mode 100644 index 89fa42fe..00000000 --- a/src/workos/types/events/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.events.models import * # noqa: F401,F403 diff --git a/src/workos/types/feature_flags/__init__.py b/src/workos/types/feature_flags/__init__.py deleted file mode 100644 index 63d36499..00000000 --- a/src/workos/types/feature_flags/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.feature_flags.models import * # noqa: F401,F403 -from workos.feature_flags.targets.models import * # noqa: F401,F403 diff --git a/src/workos/types/feature_flags/feature_flag.py b/src/workos/types/feature_flags/feature_flag.py deleted file mode 100644 index 79734b6e..00000000 --- a/src/workos/types/feature_flags/feature_flag.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.feature_flags.models import FeatureFlag as FeatureFlag diff --git a/src/workos/types/feature_flags/feature_flag_owner.py b/src/workos/types/feature_flags/feature_flag_owner.py deleted file mode 100644 index 98c51846..00000000 --- a/src/workos/types/feature_flags/feature_flag_owner.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.feature_flags.models import FeatureFlagOwner as FeatureFlagOwner diff --git a/src/workos/types/feature_flags/flag.py b/src/workos/types/feature_flags/flag.py deleted file mode 100644 index 3a3b080e..00000000 --- a/src/workos/types/feature_flags/flag.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.feature_flags.models import Flag as Flag diff --git a/src/workos/types/feature_flags/flag_owner.py b/src/workos/types/feature_flags/flag_owner.py deleted file mode 100644 index 241ad09d..00000000 --- a/src/workos/types/feature_flags/flag_owner.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.feature_flags.models import FlagOwner as FlagOwner diff --git a/src/workos/types/feature_flags_targets/__init__.py b/src/workos/types/feature_flags_targets/__init__.py deleted file mode 100644 index a0b603e2..00000000 --- a/src/workos/types/feature_flags_targets/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.feature_flags.targets.models import * # noqa: F401,F403 diff --git a/src/workos/types/fga/__init__.py b/src/workos/types/fga/__init__.py deleted file mode 100644 index 599e9b5a..00000000 --- a/src/workos/types/fga/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.authorization.models import * # noqa: F401,F403 diff --git a/src/workos/types/mfa/__init__.py b/src/workos/types/mfa/__init__.py deleted file mode 100644 index 8c057ca6..00000000 --- a/src/workos/types/mfa/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.multi_factor_auth.models import * # noqa: F401,F403 diff --git a/src/workos/types/multi_factor_auth/__init__.py b/src/workos/types/multi_factor_auth/__init__.py deleted file mode 100644 index cdc2ad05..00000000 --- a/src/workos/types/multi_factor_auth/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.multi_factor_auth.models import * # noqa: F401,F403 -from workos.multi_factor_auth.challenges.models import * # noqa: F401,F403 diff --git a/src/workos/types/multi_factor_auth/authentication_challenge.py b/src/workos/types/multi_factor_auth/authentication_challenge.py deleted file mode 100644 index e48fb135..00000000 --- a/src/workos/types/multi_factor_auth/authentication_challenge.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.multi_factor_auth.challenges.models import ( - AuthenticationChallenge as AuthenticationChallenge, -) diff --git a/src/workos/types/multi_factor_auth/authentication_challenge_verify_response.py b/src/workos/types/multi_factor_auth/authentication_challenge_verify_response.py deleted file mode 100644 index 3563f3ef..00000000 --- a/src/workos/types/multi_factor_auth/authentication_challenge_verify_response.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.multi_factor_auth.challenges.models import ( - AuthenticationChallengeVerifyResponse as AuthenticationChallengeVerifyResponse, -) diff --git a/src/workos/types/multi_factor_auth/authentication_challenges_verify_request.py b/src/workos/types/multi_factor_auth/authentication_challenges_verify_request.py deleted file mode 100644 index 4583e069..00000000 --- a/src/workos/types/multi_factor_auth/authentication_challenges_verify_request.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.multi_factor_auth.challenges.models import ( - AuthenticationChallengesVerifyRequest as AuthenticationChallengesVerifyRequest, -) diff --git a/src/workos/types/multi_factor_auth/authentication_factor.py b/src/workos/types/multi_factor_auth/authentication_factor.py deleted file mode 100644 index 7ddc28e6..00000000 --- a/src/workos/types/multi_factor_auth/authentication_factor.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.multi_factor_auth.models import AuthenticationFactor as AuthenticationFactor diff --git a/src/workos/types/multi_factor_auth/authentication_factor_enrolled.py b/src/workos/types/multi_factor_auth/authentication_factor_enrolled.py deleted file mode 100644 index ba1adbf4..00000000 --- a/src/workos/types/multi_factor_auth/authentication_factor_enrolled.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.multi_factor_auth.models import ( - AuthenticationFactorEnrolled as AuthenticationFactorEnrolled, -) diff --git a/src/workos/types/multi_factor_auth/authentication_factor_enrolled_sms.py b/src/workos/types/multi_factor_auth/authentication_factor_enrolled_sms.py deleted file mode 100644 index c711eadb..00000000 --- a/src/workos/types/multi_factor_auth/authentication_factor_enrolled_sms.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.multi_factor_auth.models import ( - AuthenticationFactorEnrolledSms as AuthenticationFactorEnrolledSms, -) diff --git a/src/workos/types/multi_factor_auth/authentication_factor_enrolled_totp.py b/src/workos/types/multi_factor_auth/authentication_factor_enrolled_totp.py deleted file mode 100644 index 9b7f0e5f..00000000 --- a/src/workos/types/multi_factor_auth/authentication_factor_enrolled_totp.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.multi_factor_auth.models import ( - AuthenticationFactorEnrolledTotp as AuthenticationFactorEnrolledTotp, -) diff --git a/src/workos/types/multi_factor_auth/authentication_factor_sms.py b/src/workos/types/multi_factor_auth/authentication_factor_sms.py deleted file mode 100644 index febc0ddf..00000000 --- a/src/workos/types/multi_factor_auth/authentication_factor_sms.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.multi_factor_auth.models import ( - AuthenticationFactorSms as AuthenticationFactorSms, -) diff --git a/src/workos/types/multi_factor_auth/authentication_factor_totp.py b/src/workos/types/multi_factor_auth/authentication_factor_totp.py deleted file mode 100644 index 51ed6dd7..00000000 --- a/src/workos/types/multi_factor_auth/authentication_factor_totp.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.multi_factor_auth.models import ( - AuthenticationFactorTotp as AuthenticationFactorTotp, -) diff --git a/src/workos/types/multi_factor_auth/authentication_factors_create_request.py b/src/workos/types/multi_factor_auth/authentication_factors_create_request.py deleted file mode 100644 index 14994ec9..00000000 --- a/src/workos/types/multi_factor_auth/authentication_factors_create_request.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.multi_factor_auth.models import ( - AuthenticationFactorsCreateRequest as AuthenticationFactorsCreateRequest, -) diff --git a/src/workos/types/multi_factor_auth/challenge_authentication_factor.py b/src/workos/types/multi_factor_auth/challenge_authentication_factor.py deleted file mode 100644 index faaf16db..00000000 --- a/src/workos/types/multi_factor_auth/challenge_authentication_factor.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.multi_factor_auth.models import ( - ChallengeAuthenticationFactor as ChallengeAuthenticationFactor, -) diff --git a/src/workos/types/multi_factor_auth_challenges/__init__.py b/src/workos/types/multi_factor_auth_challenges/__init__.py deleted file mode 100644 index 47ecdc38..00000000 --- a/src/workos/types/multi_factor_auth_challenges/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.multi_factor_auth.challenges.models import * # noqa: F401,F403 diff --git a/src/workos/types/organization_domains/__init__.py b/src/workos/types/organization_domains/__init__.py deleted file mode 100644 index aa31be5f..00000000 --- a/src/workos/types/organization_domains/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.organization_domains.models import * # noqa: F401,F403 diff --git a/src/workos/types/organizations/__init__.py b/src/workos/types/organizations/__init__.py deleted file mode 100644 index cd384055..00000000 --- a/src/workos/types/organizations/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.organizations.models import * # noqa: F401,F403 -from workos.organizations.api_keys.models import * # noqa: F401,F403 -from workos.organizations.feature_flags.models import * # noqa: F401,F403 diff --git a/src/workos/types/organizations/api_key_with_value.py b/src/workos/types/organizations/api_key_with_value.py deleted file mode 100644 index 89123ea6..00000000 --- a/src/workos/types/organizations/api_key_with_value.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.organizations.api_keys.models import ApiKeyWithValue as ApiKeyWithValue diff --git a/src/workos/types/organizations/api_key_with_value_owner.py b/src/workos/types/organizations/api_key_with_value_owner.py deleted file mode 100644 index b469d318..00000000 --- a/src/workos/types/organizations/api_key_with_value_owner.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.organizations.api_keys.models import ( - ApiKeyWithValueOwner as ApiKeyWithValueOwner, -) diff --git a/src/workos/types/organizations/audit_log_configuration.py b/src/workos/types/organizations/audit_log_configuration.py deleted file mode 100644 index c912bea8..00000000 --- a/src/workos/types/organizations/audit_log_configuration.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.organizations.models import AuditLogConfiguration as AuditLogConfiguration diff --git a/src/workos/types/organizations/audit_log_configuration_log_stream.py b/src/workos/types/organizations/audit_log_configuration_log_stream.py deleted file mode 100644 index 59221d99..00000000 --- a/src/workos/types/organizations/audit_log_configuration_log_stream.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.organizations.models import ( - AuditLogConfigurationLogStream as AuditLogConfigurationLogStream, -) diff --git a/src/workos/types/organizations/audit_logs_retention_json.py b/src/workos/types/organizations/audit_logs_retention_json.py deleted file mode 100644 index 234bdae0..00000000 --- a/src/workos/types/organizations/audit_logs_retention_json.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.organizations.models import AuditLogsRetentionJson as AuditLogsRetentionJson diff --git a/src/workos/types/organizations/create_organization_api_key.py b/src/workos/types/organizations/create_organization_api_key.py deleted file mode 100644 index 157e2122..00000000 --- a/src/workos/types/organizations/create_organization_api_key.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.organizations.api_keys.models import ( - CreateOrganizationApiKey as CreateOrganizationApiKey, -) diff --git a/src/workos/types/organizations/organization.py b/src/workos/types/organizations/organization.py deleted file mode 100644 index 70ff31b6..00000000 --- a/src/workos/types/organizations/organization.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.organizations.models import Organization as Organization diff --git a/src/workos/types/organizations/organization_domain_data.py b/src/workos/types/organizations/organization_domain_data.py deleted file mode 100644 index ea3e19c8..00000000 --- a/src/workos/types/organizations/organization_domain_data.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.organizations.models import OrganizationDomainData as OrganizationDomainData diff --git a/src/workos/types/organizations/organization_dto.py b/src/workos/types/organizations/organization_dto.py deleted file mode 100644 index 4707160a..00000000 --- a/src/workos/types/organizations/organization_dto.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.organizations.models import OrganizationDto as OrganizationDto diff --git a/src/workos/types/organizations/update_audit_logs_retention.py b/src/workos/types/organizations/update_audit_logs_retention.py deleted file mode 100644 index 3667d6c5..00000000 --- a/src/workos/types/organizations/update_audit_logs_retention.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.organizations.models import ( - UpdateAuditLogsRetention as UpdateAuditLogsRetention, -) diff --git a/src/workos/types/organizations/update_organization.py b/src/workos/types/organizations/update_organization.py deleted file mode 100644 index 8bd8626c..00000000 --- a/src/workos/types/organizations/update_organization.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.organizations.models import UpdateOrganization as UpdateOrganization diff --git a/src/workos/types/organizations_api_keys/__init__.py b/src/workos/types/organizations_api_keys/__init__.py deleted file mode 100644 index 114376c7..00000000 --- a/src/workos/types/organizations_api_keys/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.organizations.api_keys.models import * # noqa: F401,F403 diff --git a/src/workos/types/organizations_feature_flags/__init__.py b/src/workos/types/organizations_feature_flags/__init__.py deleted file mode 100644 index b1e1a0a6..00000000 --- a/src/workos/types/organizations_feature_flags/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.organizations.feature_flags.models import * # noqa: F401,F403 diff --git a/src/workos/types/permissions/__init__.py b/src/workos/types/permissions/__init__.py deleted file mode 100644 index 6c9dc9d4..00000000 --- a/src/workos/types/permissions/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.permissions.models import * # noqa: F401,F403 diff --git a/src/workos/types/pipes/__init__.py b/src/workos/types/pipes/__init__.py deleted file mode 100644 index dd3c36bd..00000000 --- a/src/workos/types/pipes/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.pipes.models import * # noqa: F401,F403 diff --git a/src/workos/types/portal/__init__.py b/src/workos/types/portal/__init__.py deleted file mode 100644 index d183d123..00000000 --- a/src/workos/types/portal/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.admin_portal.models import * # noqa: F401,F403 diff --git a/src/workos/types/radar/__init__.py b/src/workos/types/radar/__init__.py deleted file mode 100644 index 78661b8a..00000000 --- a/src/workos/types/radar/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.radar.models import * # noqa: F401,F403 diff --git a/src/workos/types/sso/__init__.py b/src/workos/types/sso/__init__.py deleted file mode 100644 index c16bc168..00000000 --- a/src/workos/types/sso/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.sso.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management/__init__.py b/src/workos/types/user_management/__init__.py deleted file mode 100644 index 28030de7..00000000 --- a/src/workos/types/user_management/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.authentication.models import * # noqa: F401,F403 -from workos.user_management.cors_origins.models import * # noqa: F401,F403 -from workos.user_management.data_providers.models import * # noqa: F401,F403 -from workos.user_management.invitations.models import * # noqa: F401,F403 -from workos.user_management.jwt_template.models import * # noqa: F401,F403 -from workos.user_management.magic_auth.models import * # noqa: F401,F403 -from workos.user_management.multi_factor_authentication.models import * # noqa: F401,F403 -from workos.user_management.organization_membership.models import * # noqa: F401,F403 -from workos.user_management.redirect_uris.models import * # noqa: F401,F403 -from workos.user_management.session_tokens.models import * # noqa: F401,F403 -from workos.user_management.users.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management/authenticate_response.py b/src/workos/types/user_management/authenticate_response.py deleted file mode 100644 index 0aa9eaff..00000000 --- a/src/workos/types/user_management/authenticate_response.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.authentication.models import ( - AuthenticateResponse as AuthenticateResponse, -) diff --git a/src/workos/types/user_management/authenticate_response_impersonator.py b/src/workos/types/user_management/authenticate_response_impersonator.py deleted file mode 100644 index d1f8cd6d..00000000 --- a/src/workos/types/user_management/authenticate_response_impersonator.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.authentication.models import ( - AuthenticateResponseImpersonator as AuthenticateResponseImpersonator, -) diff --git a/src/workos/types/user_management/authenticate_response_oauth_token.py b/src/workos/types/user_management/authenticate_response_oauth_token.py deleted file mode 100644 index efda7f93..00000000 --- a/src/workos/types/user_management/authenticate_response_oauth_token.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.authentication.models import ( - AuthenticateResponseOAuthToken as AuthenticateResponseOAuthToken, -) diff --git a/src/workos/types/user_management/authorization_code_session_authenticate_request.py b/src/workos/types/user_management/authorization_code_session_authenticate_request.py deleted file mode 100644 index 585beca5..00000000 --- a/src/workos/types/user_management/authorization_code_session_authenticate_request.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.authentication.models import ( - AuthorizationCodeSessionAuthenticateRequest as AuthorizationCodeSessionAuthenticateRequest, -) diff --git a/src/workos/types/user_management/connected_account.py b/src/workos/types/user_management/connected_account.py deleted file mode 100644 index 15350b69..00000000 --- a/src/workos/types/user_management/connected_account.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.data_providers.models import ( - ConnectedAccount as ConnectedAccount, -) diff --git a/src/workos/types/user_management/cors_origin_response.py b/src/workos/types/user_management/cors_origin_response.py deleted file mode 100644 index b5b3b2cc..00000000 --- a/src/workos/types/user_management/cors_origin_response.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.cors_origins.models import ( - CORSOriginResponse as CORSOriginResponse, -) diff --git a/src/workos/types/user_management/create_cors_origin.py b/src/workos/types/user_management/create_cors_origin.py deleted file mode 100644 index 9c30fedd..00000000 --- a/src/workos/types/user_management/create_cors_origin.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.cors_origins.models import ( - CreateCORSOrigin as CreateCORSOrigin, -) diff --git a/src/workos/types/user_management/create_magic_code_and_return.py b/src/workos/types/user_management/create_magic_code_and_return.py deleted file mode 100644 index e709342e..00000000 --- a/src/workos/types/user_management/create_magic_code_and_return.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.magic_auth.models import ( - CreateMagicCodeAndReturn as CreateMagicCodeAndReturn, -) diff --git a/src/workos/types/user_management/create_password_reset.py b/src/workos/types/user_management/create_password_reset.py deleted file mode 100644 index f3feae17..00000000 --- a/src/workos/types/user_management/create_password_reset.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.users.models import ( - CreatePasswordReset as CreatePasswordReset, -) diff --git a/src/workos/types/user_management/create_password_reset_token.py b/src/workos/types/user_management/create_password_reset_token.py deleted file mode 100644 index 2ba89b3f..00000000 --- a/src/workos/types/user_management/create_password_reset_token.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.users.models import ( - CreatePasswordResetToken as CreatePasswordResetToken, -) diff --git a/src/workos/types/user_management/create_redirect_uri.py b/src/workos/types/user_management/create_redirect_uri.py deleted file mode 100644 index a28ede70..00000000 --- a/src/workos/types/user_management/create_redirect_uri.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.redirect_uris.models import ( - CreateRedirectUri as CreateRedirectUri, -) diff --git a/src/workos/types/user_management/create_user.py b/src/workos/types/user_management/create_user.py deleted file mode 100644 index d1976ab0..00000000 --- a/src/workos/types/user_management/create_user.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.users.models import CreateUser as CreateUser diff --git a/src/workos/types/user_management/create_user_invite_options.py b/src/workos/types/user_management/create_user_invite_options.py deleted file mode 100644 index 155b5691..00000000 --- a/src/workos/types/user_management/create_user_invite_options.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.invitations.models import ( - CreateUserInviteOptions as CreateUserInviteOptions, -) diff --git a/src/workos/types/user_management/create_user_organization_membership.py b/src/workos/types/user_management/create_user_organization_membership.py deleted file mode 100644 index 122e4259..00000000 --- a/src/workos/types/user_management/create_user_organization_membership.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.organization_membership.models import ( - CreateUserOrganizationMembership as CreateUserOrganizationMembership, -) diff --git a/src/workos/types/user_management/data_integrations_list_response.py b/src/workos/types/user_management/data_integrations_list_response.py deleted file mode 100644 index 763601c3..00000000 --- a/src/workos/types/user_management/data_integrations_list_response.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.data_providers.models import ( - DataIntegrationsListResponse as DataIntegrationsListResponse, -) diff --git a/src/workos/types/user_management/data_integrations_list_response_data.py b/src/workos/types/user_management/data_integrations_list_response_data.py deleted file mode 100644 index b25dd3f2..00000000 --- a/src/workos/types/user_management/data_integrations_list_response_data.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.data_providers.models import ( - DataIntegrationsListResponseData as DataIntegrationsListResponseData, -) diff --git a/src/workos/types/user_management/data_integrations_list_response_data_connected_account.py b/src/workos/types/user_management/data_integrations_list_response_data_connected_account.py deleted file mode 100644 index 66167b4b..00000000 --- a/src/workos/types/user_management/data_integrations_list_response_data_connected_account.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.data_providers.models import ( - DataIntegrationsListResponseDataConnectedAccount as DataIntegrationsListResponseDataConnectedAccount, -) diff --git a/src/workos/types/user_management/device_authorization_response.py b/src/workos/types/user_management/device_authorization_response.py deleted file mode 100644 index 81ec6786..00000000 --- a/src/workos/types/user_management/device_authorization_response.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.authentication.models import ( - DeviceAuthorizationResponse as DeviceAuthorizationResponse, -) diff --git a/src/workos/types/user_management/email_verification.py b/src/workos/types/user_management/email_verification.py deleted file mode 100644 index fef458a1..00000000 --- a/src/workos/types/user_management/email_verification.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.users.models import EmailVerification as EmailVerification diff --git a/src/workos/types/user_management/enroll_user_authentication_factor.py b/src/workos/types/user_management/enroll_user_authentication_factor.py deleted file mode 100644 index 9a72c5f4..00000000 --- a/src/workos/types/user_management/enroll_user_authentication_factor.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.multi_factor_authentication.models import ( - EnrollUserAuthenticationFactor as EnrollUserAuthenticationFactor, -) diff --git a/src/workos/types/user_management/invitation.py b/src/workos/types/user_management/invitation.py deleted file mode 100644 index a19b7412..00000000 --- a/src/workos/types/user_management/invitation.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.invitations.models import Invitation as Invitation diff --git a/src/workos/types/user_management/jwks_response.py b/src/workos/types/user_management/jwks_response.py deleted file mode 100644 index ef78489e..00000000 --- a/src/workos/types/user_management/jwks_response.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.session_tokens.models import JwksResponse as JwksResponse diff --git a/src/workos/types/user_management/jwks_response_keys.py b/src/workos/types/user_management/jwks_response_keys.py deleted file mode 100644 index 10ea04cf..00000000 --- a/src/workos/types/user_management/jwks_response_keys.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.session_tokens.models import ( - JwksResponseKeys as JwksResponseKeys, -) diff --git a/src/workos/types/user_management/jwt_template_response.py b/src/workos/types/user_management/jwt_template_response.py deleted file mode 100644 index 1e071697..00000000 --- a/src/workos/types/user_management/jwt_template_response.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.jwt_template.models import ( - JWTTemplateResponse as JWTTemplateResponse, -) diff --git a/src/workos/types/user_management/magic_auth.py b/src/workos/types/user_management/magic_auth.py deleted file mode 100644 index ed7a4d0c..00000000 --- a/src/workos/types/user_management/magic_auth.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.magic_auth.models import MagicAuth as MagicAuth diff --git a/src/workos/types/user_management/organization_membership.py b/src/workos/types/user_management/organization_membership.py deleted file mode 100644 index 5a2af512..00000000 --- a/src/workos/types/user_management/organization_membership.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.organization_membership.models import ( - OrganizationMembership as OrganizationMembership, -) diff --git a/src/workos/types/user_management/password_reset.py b/src/workos/types/user_management/password_reset.py deleted file mode 100644 index ba2ffa3d..00000000 --- a/src/workos/types/user_management/password_reset.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.users.models import PasswordReset as PasswordReset diff --git a/src/workos/types/user_management/password_session_authenticate_request.py b/src/workos/types/user_management/password_session_authenticate_request.py deleted file mode 100644 index b5809eef..00000000 --- a/src/workos/types/user_management/password_session_authenticate_request.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.authentication.models import ( - PasswordSessionAuthenticateRequest as PasswordSessionAuthenticateRequest, -) diff --git a/src/workos/types/user_management/redirect_uri.py b/src/workos/types/user_management/redirect_uri.py deleted file mode 100644 index cd0c6453..00000000 --- a/src/workos/types/user_management/redirect_uri.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.redirect_uris.models import RedirectUri as RedirectUri diff --git a/src/workos/types/user_management/refresh_token_session_authenticate_request.py b/src/workos/types/user_management/refresh_token_session_authenticate_request.py deleted file mode 100644 index aba5c4ca..00000000 --- a/src/workos/types/user_management/refresh_token_session_authenticate_request.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.authentication.models import ( - RefreshTokenSessionAuthenticateRequest as RefreshTokenSessionAuthenticateRequest, -) diff --git a/src/workos/types/user_management/resend_user_invite_options.py b/src/workos/types/user_management/resend_user_invite_options.py deleted file mode 100644 index fb61163e..00000000 --- a/src/workos/types/user_management/resend_user_invite_options.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.invitations.models import ( - ResendUserInviteOptions as ResendUserInviteOptions, -) diff --git a/src/workos/types/user_management/reset_password_response.py b/src/workos/types/user_management/reset_password_response.py deleted file mode 100644 index 91daa530..00000000 --- a/src/workos/types/user_management/reset_password_response.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.users.models import ( - ResetPasswordResponse as ResetPasswordResponse, -) diff --git a/src/workos/types/user_management/revoke_session.py b/src/workos/types/user_management/revoke_session.py deleted file mode 100644 index f747f077..00000000 --- a/src/workos/types/user_management/revoke_session.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.authentication.models import RevokeSession as RevokeSession diff --git a/src/workos/types/user_management/send_verification_email_response.py b/src/workos/types/user_management/send_verification_email_response.py deleted file mode 100644 index 58a49100..00000000 --- a/src/workos/types/user_management/send_verification_email_response.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.users.models import ( - SendVerificationEmailResponse as SendVerificationEmailResponse, -) diff --git a/src/workos/types/user_management/sso_device_authorization_request.py b/src/workos/types/user_management/sso_device_authorization_request.py deleted file mode 100644 index c0125940..00000000 --- a/src/workos/types/user_management/sso_device_authorization_request.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.authentication.models import ( - SSODeviceAuthorizationRequest as SSODeviceAuthorizationRequest, -) diff --git a/src/workos/types/user_management/update_jwt_template.py b/src/workos/types/user_management/update_jwt_template.py deleted file mode 100644 index ce69d7ee..00000000 --- a/src/workos/types/user_management/update_jwt_template.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.jwt_template.models import ( - UpdateJWTTemplate as UpdateJWTTemplate, -) diff --git a/src/workos/types/user_management/update_user.py b/src/workos/types/user_management/update_user.py deleted file mode 100644 index df7312f0..00000000 --- a/src/workos/types/user_management/update_user.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.users.models import UpdateUser as UpdateUser diff --git a/src/workos/types/user_management/update_user_organization_membership.py b/src/workos/types/user_management/update_user_organization_membership.py deleted file mode 100644 index 56e23ae7..00000000 --- a/src/workos/types/user_management/update_user_organization_membership.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.organization_membership.models import ( - UpdateUserOrganizationMembership as UpdateUserOrganizationMembership, -) diff --git a/src/workos/types/user_management/urn_ietf_params_oauth_grant_type_device_code_session_authenticate_request.py b/src/workos/types/user_management/urn_ietf_params_oauth_grant_type_device_code_session_authenticate_request.py deleted file mode 100644 index 14ca100d..00000000 --- a/src/workos/types/user_management/urn_ietf_params_oauth_grant_type_device_code_session_authenticate_request.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.authentication.models import ( - UrnIetfParamsOAuthGrantTypeDeviceCodeSessionAuthenticateRequest as UrnIetfParamsOAuthGrantTypeDeviceCodeSessionAuthenticateRequest, -) diff --git a/src/workos/types/user_management/urn_workos_oauth_grant_type_email_verification_code_session_authenticate_request.py b/src/workos/types/user_management/urn_workos_oauth_grant_type_email_verification_code_session_authenticate_request.py deleted file mode 100644 index e1d27e99..00000000 --- a/src/workos/types/user_management/urn_workos_oauth_grant_type_email_verification_code_session_authenticate_request.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.authentication.models import ( - UrnWorkOSOAuthGrantTypeEmailVerificationCodeSessionAuthenticateRequest as UrnWorkOSOAuthGrantTypeEmailVerificationCodeSessionAuthenticateRequest, -) diff --git a/src/workos/types/user_management/urn_workos_oauth_grant_type_magic_auth_code_session_authenticate_request.py b/src/workos/types/user_management/urn_workos_oauth_grant_type_magic_auth_code_session_authenticate_request.py deleted file mode 100644 index b1627305..00000000 --- a/src/workos/types/user_management/urn_workos_oauth_grant_type_magic_auth_code_session_authenticate_request.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.authentication.models import ( - UrnWorkOSOAuthGrantTypeMagicAuthCodeSessionAuthenticateRequest as UrnWorkOSOAuthGrantTypeMagicAuthCodeSessionAuthenticateRequest, -) diff --git a/src/workos/types/user_management/urn_workos_oauth_grant_type_mfa_totp_session_authenticate_request.py b/src/workos/types/user_management/urn_workos_oauth_grant_type_mfa_totp_session_authenticate_request.py deleted file mode 100644 index 51df7f3b..00000000 --- a/src/workos/types/user_management/urn_workos_oauth_grant_type_mfa_totp_session_authenticate_request.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.authentication.models import ( - UrnWorkOSOAuthGrantTypeMFATotpSessionAuthenticateRequest as UrnWorkOSOAuthGrantTypeMFATotpSessionAuthenticateRequest, -) diff --git a/src/workos/types/user_management/urn_workos_oauth_grant_type_organization_selection_session_authenticate_request.py b/src/workos/types/user_management/urn_workos_oauth_grant_type_organization_selection_session_authenticate_request.py deleted file mode 100644 index 14e8d990..00000000 --- a/src/workos/types/user_management/urn_workos_oauth_grant_type_organization_selection_session_authenticate_request.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.authentication.models import ( - UrnWorkOSOAuthGrantTypeOrganizationSelectionSessionAuthenticateRequest as UrnWorkOSOAuthGrantTypeOrganizationSelectionSessionAuthenticateRequest, -) diff --git a/src/workos/types/user_management/user.py b/src/workos/types/user_management/user.py deleted file mode 100644 index ac1834bd..00000000 --- a/src/workos/types/user_management/user.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.authentication.models import User as User diff --git a/src/workos/types/user_management/user_authentication_factor_enroll_response.py b/src/workos/types/user_management/user_authentication_factor_enroll_response.py deleted file mode 100644 index efe6016f..00000000 --- a/src/workos/types/user_management/user_authentication_factor_enroll_response.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.multi_factor_authentication.models import ( - UserAuthenticationFactorEnrollResponse as UserAuthenticationFactorEnrollResponse, -) diff --git a/src/workos/types/user_management/user_identities_get_item.py b/src/workos/types/user_management/user_identities_get_item.py deleted file mode 100644 index c12ce0b0..00000000 --- a/src/workos/types/user_management/user_identities_get_item.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.users.models import ( - UserIdentitiesGetItem as UserIdentitiesGetItem, -) diff --git a/src/workos/types/user_management/user_invite.py b/src/workos/types/user_management/user_invite.py deleted file mode 100644 index 68c0c6a7..00000000 --- a/src/workos/types/user_management/user_invite.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.invitations.models import UserInvite as UserInvite diff --git a/src/workos/types/user_management/user_organization_membership.py b/src/workos/types/user_management/user_organization_membership.py deleted file mode 100644 index 6231353e..00000000 --- a/src/workos/types/user_management/user_organization_membership.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.organization_membership.models import ( - UserOrganizationMembership as UserOrganizationMembership, -) diff --git a/src/workos/types/user_management/user_sessions_impersonator.py b/src/workos/types/user_management/user_sessions_impersonator.py deleted file mode 100644 index cb0efa1a..00000000 --- a/src/workos/types/user_management/user_sessions_impersonator.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.users.models import ( - UserSessionsImpersonator as UserSessionsImpersonator, -) diff --git a/src/workos/types/user_management/user_sessions_list_item.py b/src/workos/types/user_management/user_sessions_list_item.py deleted file mode 100644 index d2eae33c..00000000 --- a/src/workos/types/user_management/user_sessions_list_item.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.users.models import ( - UserSessionsListItem as UserSessionsListItem, -) diff --git a/src/workos/types/user_management/verify_email_address.py b/src/workos/types/user_management/verify_email_address.py deleted file mode 100644 index fa3bde3a..00000000 --- a/src/workos/types/user_management/verify_email_address.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.users.models import VerifyEmailAddress as VerifyEmailAddress diff --git a/src/workos/types/user_management/verify_email_response.py b/src/workos/types/user_management/verify_email_response.py deleted file mode 100644 index 0068e16e..00000000 --- a/src/workos/types/user_management/verify_email_response.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.users.models import ( - VerifyEmailResponse as VerifyEmailResponse, -) diff --git a/src/workos/types/user_management_authentication/__init__.py b/src/workos/types/user_management_authentication/__init__.py deleted file mode 100644 index 76f8d65a..00000000 --- a/src/workos/types/user_management_authentication/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.authentication.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_cors_origins/__init__.py b/src/workos/types/user_management_cors_origins/__init__.py deleted file mode 100644 index 4a72e6d7..00000000 --- a/src/workos/types/user_management_cors_origins/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.cors_origins.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_data_providers/__init__.py b/src/workos/types/user_management_data_providers/__init__.py deleted file mode 100644 index 4bca66a1..00000000 --- a/src/workos/types/user_management_data_providers/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.data_providers.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_invitations/__init__.py b/src/workos/types/user_management_invitations/__init__.py deleted file mode 100644 index 1d575bad..00000000 --- a/src/workos/types/user_management_invitations/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.invitations.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_jwt_template/__init__.py b/src/workos/types/user_management_jwt_template/__init__.py deleted file mode 100644 index a596c8ea..00000000 --- a/src/workos/types/user_management_jwt_template/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.jwt_template.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_magic_auth/__init__.py b/src/workos/types/user_management_magic_auth/__init__.py deleted file mode 100644 index b9f6dcf7..00000000 --- a/src/workos/types/user_management_magic_auth/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.magic_auth.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_multi_factor_authentication/__init__.py b/src/workos/types/user_management_multi_factor_authentication/__init__.py deleted file mode 100644 index c2c2ee13..00000000 --- a/src/workos/types/user_management_multi_factor_authentication/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.multi_factor_authentication.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_organization_membership/__init__.py b/src/workos/types/user_management_organization_membership/__init__.py deleted file mode 100644 index d24e1869..00000000 --- a/src/workos/types/user_management_organization_membership/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.organization_membership.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_redirect_uris/__init__.py b/src/workos/types/user_management_redirect_uris/__init__.py deleted file mode 100644 index f752307e..00000000 --- a/src/workos/types/user_management_redirect_uris/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.redirect_uris.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_session_tokens/__init__.py b/src/workos/types/user_management_session_tokens/__init__.py deleted file mode 100644 index 438389b3..00000000 --- a/src/workos/types/user_management_session_tokens/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management.session_tokens.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_users/__init__.py b/src/workos/types/user_management_users/__init__.py deleted file mode 100644 index 2d31c22c..00000000 --- a/src/workos/types/user_management_users/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management_users.authorized_applications.models import * # noqa: F401,F403 -from workos.user_management_users.feature_flags.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_users/authorized_connect_application_list_data.py b/src/workos/types/user_management_users/authorized_connect_application_list_data.py deleted file mode 100644 index ed1f6db2..00000000 --- a/src/workos/types/user_management_users/authorized_connect_application_list_data.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management_users.authorized_applications.models import ( - AuthorizedConnectApplicationListData as AuthorizedConnectApplicationListData, -) diff --git a/src/workos/types/user_management_users_authorized_applications/__init__.py b/src/workos/types/user_management_users_authorized_applications/__init__.py deleted file mode 100644 index 5d19dad3..00000000 --- a/src/workos/types/user_management_users_authorized_applications/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management_users.authorized_applications.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_users_feature_flags/__init__.py b/src/workos/types/user_management_users_feature_flags/__init__.py deleted file mode 100644 index a2bd89c7..00000000 --- a/src/workos/types/user_management_users_feature_flags/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.user_management_users.feature_flags.models import * # noqa: F401,F403 diff --git a/src/workos/types/webhooks/__init__.py b/src/workos/types/webhooks/__init__.py deleted file mode 100644 index 0b47a41b..00000000 --- a/src/workos/types/webhooks/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.webhooks.models import * # noqa: F401,F403 diff --git a/src/workos/types/widgets/__init__.py b/src/workos/types/widgets/__init__.py deleted file mode 100644 index 0daafdc6..00000000 --- a/src/workos/types/widgets/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.widgets.models import * # noqa: F401,F403 diff --git a/src/workos/types/workos_connect/__init__.py b/src/workos/types/workos_connect/__init__.py deleted file mode 100644 index 96cf742d..00000000 --- a/src/workos/types/workos_connect/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file is auto-generated by oagen. Do not edit. - -from workos.workos_connect.models import * # noqa: F401,F403 From d78dc9dac065ddaa73eb6b9db605cee85b9e82ee Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 31 Mar 2026 14:50:34 -0400 Subject: [PATCH 18/26] restore some aliases --- src/workos/__init__.py | 5 +++++ src/workos/_client.py | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/workos/__init__.py b/src/workos/__init__.py index 0963f2d5..ff8a0da9 100644 --- a/src/workos/__init__.py +++ b/src/workos/__init__.py @@ -20,9 +20,14 @@ from ._pagination import AsyncPage, SyncPage from ._types import RequestOptions +WorkOS = WorkOSClient +AsyncWorkOS = AsyncWorkOSClient + __all__ = [ "WorkOSClient", "AsyncWorkOSClient", + "WorkOS", + "AsyncWorkOS", "RequestOptions", "BaseRequestException", "AuthenticationException", diff --git a/src/workos/_client.py b/src/workos/_client.py index bc491ddb..007c6c23 100644 --- a/src/workos/_client.py +++ b/src/workos/_client.py @@ -121,6 +121,7 @@ from .webhooks._resource import Webhooks, AsyncWebhooks from .widgets._resource import Widgets, AsyncWidgets from .audit_logs._resource import AuditLogs, AsyncAuditLogs +from .session import AsyncSession, Session try: from importlib.metadata import version as _pkg_version @@ -223,6 +224,15 @@ def data_providers(self) -> UserManagementDataProviders: def multi_factor_authentication(self) -> UserManagementMultiFactorAuthentication: return UserManagementMultiFactorAuthentication(self._client) + def load_sealed_session( + self, *, sealed_session: str, cookie_password: str + ) -> Session: + return Session( + client=self._client, + session_data=sealed_session, + cookie_password=cookie_password, + ) + class UserManagementUsersNamespace(object): """UserManagementUsers resources.""" @@ -328,6 +338,15 @@ def multi_factor_authentication( ) -> AsyncUserManagementMultiFactorAuthentication: return AsyncUserManagementMultiFactorAuthentication(self._client) + def load_sealed_session( + self, *, sealed_session: str, cookie_password: str + ) -> AsyncSession: + return AsyncSession( + client=self._client, + session_data=sealed_session, + cookie_password=cookie_password, + ) + class AsyncUserManagementUsersNamespace(object): """UserManagementUsers resources (async).""" @@ -1086,3 +1105,8 @@ async def _fetch(*, after: Optional[str] = None) -> AsyncPage[D]: ) return AsyncPage(data=items, list_metadata=list_metadata, _fetch_page=_fetch) + + +# Top-level client aliases retained for SDK ergonomics and internal typing +WorkOS = WorkOSClient +AsyncWorkOS = AsyncWorkOSClient From 1be98d57e54fc86e083924e65bf1fcd846c545a7 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 31 Mar 2026 16:42:55 -0400 Subject: [PATCH 19/26] chore(python): regenerate sdk from updated emitter --- src/workos/_client.py | 87 +++++- src/workos/_errors.py | 291 ++++++------------ src/workos/_types.py | 6 + src/workos/admin_portal/_resource.py | 24 +- src/workos/api_keys/_resource.py | 2 +- .../application_client_secrets/_resource.py | 2 +- src/workos/applications/_resource.py | 10 +- src/workos/audit_logs/_resource.py | 32 +- src/workos/audit_logs/models/__init__.py | 3 + .../models/applications_order_literal.py | 6 + src/workos/authorization/_resource.py | 144 ++++++--- src/workos/authorization/models/__init__.py | 4 +- .../authorization_assignment_literal.py | 6 + src/workos/common/__init__.py | 80 ++++- src/workos/common/models/__init__.py | 92 +++++- ..._configuration_log_stream_state_literal.py | 8 + ...g_configuration_log_stream_type_literal.py | 6 + .../audit_log_configuration_state_literal.py | 6 + .../audit_log_export_json_state_literal.py | 6 + ..._response_authentication_method_literal.py | 10 + ...entication_factor_enrolled_type_literal.py | 6 + ...ion_factors_create_request_type_literal.py | 8 + .../models/connected_account_state_literal.py | 6 + .../common/models/connection_state_literal.py | 6 + .../models/connection_status_literal.py | 6 + .../common/models/connection_type_literal.py | 6 + ...ate_user_dto_password_hash_type_literal.py | 6 + ..._user_invite_options_dto_locale_literal.py | 6 + ...ate_webhook_endpoint_dto_events_literal.py | 6 + ...ns_list_response_data_ownership_literal.py | 10 + .../common/models/directory_state_literal.py | 6 + .../common/models/directory_type_literal.py | 6 + ...irectory_user_with_groups_state_literal.py | 6 + .../generate_link_dto_intent_literal.py | 6 + .../common/models/invitation_state_literal.py | 6 + .../common/models/list_data_type_literal.py | 6 + ...anization_domain_data_dto_state_literal.py | 6 + ...zation_domain_stand_alone_state_literal.py | 6 + ...and_alone_verification_strategy_literal.py | 10 + ...tandalone_assess_request_action_literal.py | 6 + ...lone_assess_request_auth_method_literal.py | 8 + ...dar_standalone_response_control_literal.py | 6 + ...dar_standalone_response_verdict_literal.py | 6 + ...ate_webhook_endpoint_dto_status_literal.py | 6 + ...er_identities_get_item_provider_literal.py | 6 + .../user_sessions_auth_method_literal.py | 6 + .../models/user_sessions_status_literal.py | 6 + ...widget_session_token_dto_scopes_literal.py | 6 + src/workos/connections/_resource.py | 24 +- src/workos/connections/models/__init__.py | 3 + .../connections_connection_type_literal.py | 6 + src/workos/directories/_resource.py | 12 +- src/workos/directory_groups/_resource.py | 12 +- src/workos/directory_users/_resource.py | 12 +- src/workos/events/_resource.py | 16 +- src/workos/events/models/event.py | 38 +-- src/workos/feature_flags/_resource.py | 12 +- src/workos/multi_factor_auth/_resource.py | 12 +- .../multi_factor_auth/challenges/_resource.py | 2 +- src/workos/organization_domains/_resource.py | 6 +- src/workos/organizations/_resource.py | 12 +- .../organizations/api_keys/_resource.py | 12 +- .../organizations/feature_flags/_resource.py | 12 +- .../organizations/models/organization.py | 17 +- src/workos/permissions/_resource.py | 12 +- src/workos/pipes/_resource.py | 2 +- src/workos/radar/_resource.py | 36 +-- src/workos/radar/models/__init__.py | 4 + .../radar/models/radar_action_literal.py | 6 + ...ndalone_response_blocklist_type_literal.py | 6 + src/workos/sso/_resource.py | 34 +- src/workos/sso/models/__init__.py | 1 + src/workos/sso/models/sso_provider_literal.py | 6 + .../authentication/_resource.py | 26 +- .../authentication/models/__init__.py | 7 +- ...agement_authentication_provider_literal.py | 8 + ...ment_authentication_screen_hint_literal.py | 8 + .../user_management/cors_origins/_resource.py | 2 +- .../data_providers/_resource.py | 2 +- .../user_management/invitations/_resource.py | 28 +- .../user_management/jwt_template/_resource.py | 2 +- .../user_management/magic_auth/_resource.py | 2 +- .../multi_factor_authentication/_resource.py | 16 +- .../organization_membership/_resource.py | 20 +- .../models/__init__.py | 3 + .../organization_membership_status_literal.py | 6 + .../redirect_uris/_resource.py | 2 +- .../session_tokens/_resource.py | 2 +- src/workos/user_management/users/_resource.py | 44 +-- .../authorized_applications/_resource.py | 16 +- .../feature_flags/_resource.py | 12 +- src/workos/webhooks/_resource.py | 26 +- src/workos/widgets/_resource.py | 8 +- src/workos/workos_connect/_resource.py | 2 +- tests/fixtures/event.json | 30 +- tests/fixtures/list_event.json | 30 +- tests/test_admin_portal.py | 48 +-- tests/test_audit_logs.py | 40 +-- tests/test_authorization.py | 84 ++--- tests/test_events.py | 56 ++-- tests/test_models_round_trip.py | 208 +++++-------- tests/test_organization_domains.py | 10 +- tests/test_sso.py | 8 +- 103 files changed, 1241 insertions(+), 835 deletions(-) create mode 100644 src/workos/audit_logs/models/applications_order_literal.py create mode 100644 src/workos/authorization/models/authorization_assignment_literal.py create mode 100644 src/workos/common/models/audit_log_configuration_log_stream_state_literal.py create mode 100644 src/workos/common/models/audit_log_configuration_log_stream_type_literal.py create mode 100644 src/workos/common/models/audit_log_configuration_state_literal.py create mode 100644 src/workos/common/models/audit_log_export_json_state_literal.py create mode 100644 src/workos/common/models/authenticate_response_authentication_method_literal.py create mode 100644 src/workos/common/models/authentication_factor_enrolled_type_literal.py create mode 100644 src/workos/common/models/authentication_factors_create_request_type_literal.py create mode 100644 src/workos/common/models/connected_account_state_literal.py create mode 100644 src/workos/common/models/connection_state_literal.py create mode 100644 src/workos/common/models/connection_status_literal.py create mode 100644 src/workos/common/models/connection_type_literal.py create mode 100644 src/workos/common/models/create_user_dto_password_hash_type_literal.py create mode 100644 src/workos/common/models/create_user_invite_options_dto_locale_literal.py create mode 100644 src/workos/common/models/create_webhook_endpoint_dto_events_literal.py create mode 100644 src/workos/common/models/data_integrations_list_response_data_ownership_literal.py create mode 100644 src/workos/common/models/directory_state_literal.py create mode 100644 src/workos/common/models/directory_type_literal.py create mode 100644 src/workos/common/models/directory_user_with_groups_state_literal.py create mode 100644 src/workos/common/models/generate_link_dto_intent_literal.py create mode 100644 src/workos/common/models/invitation_state_literal.py create mode 100644 src/workos/common/models/list_data_type_literal.py create mode 100644 src/workos/common/models/organization_domain_data_dto_state_literal.py create mode 100644 src/workos/common/models/organization_domain_stand_alone_state_literal.py create mode 100644 src/workos/common/models/organization_domain_stand_alone_verification_strategy_literal.py create mode 100644 src/workos/common/models/radar_standalone_assess_request_action_literal.py create mode 100644 src/workos/common/models/radar_standalone_assess_request_auth_method_literal.py create mode 100644 src/workos/common/models/radar_standalone_response_control_literal.py create mode 100644 src/workos/common/models/radar_standalone_response_verdict_literal.py create mode 100644 src/workos/common/models/update_webhook_endpoint_dto_status_literal.py create mode 100644 src/workos/common/models/user_identities_get_item_provider_literal.py create mode 100644 src/workos/common/models/user_sessions_auth_method_literal.py create mode 100644 src/workos/common/models/user_sessions_status_literal.py create mode 100644 src/workos/common/models/widget_session_token_dto_scopes_literal.py create mode 100644 src/workos/connections/models/connections_connection_type_literal.py create mode 100644 src/workos/radar/models/radar_action_literal.py create mode 100644 src/workos/radar/models/radar_standalone_response_blocklist_type_literal.py create mode 100644 src/workos/sso/models/sso_provider_literal.py create mode 100644 src/workos/user_management/authentication/models/user_management_authentication_provider_literal.py create mode 100644 src/workos/user_management/authentication/models/user_management_authentication_screen_hint_literal.py create mode 100644 src/workos/user_management/organization_membership/models/organization_membership_status_literal.py diff --git a/src/workos/_client.py b/src/workos/_client.py index 007c6c23..73b7e95f 100644 --- a/src/workos/_client.py +++ b/src/workos/_client.py @@ -121,7 +121,9 @@ from .webhooks._resource import Webhooks, AsyncWebhooks from .widgets._resource import Widgets, AsyncWidgets from .audit_logs._resource import AuditLogs, AsyncAuditLogs +from .passwordless import AsyncPasswordless, Passwordless from .session import AsyncSession, Session +from .vault import AsyncVault, Vault try: from importlib.metadata import version as _pkg_version @@ -147,6 +149,17 @@ def __init__(self, client: "WorkOSClient") -> None: def challenges(self) -> MultiFactorAuthChallenges: return MultiFactorAuthChallenges(self._client) + def verify_challenge( + self, + *, + authentication_challenge_id: str, + code: str, + request_options: Optional[RequestOptions] = None, + ) -> Any: + return self.challenges.verify( + authentication_challenge_id, code=code, request_options=request_options + ) + class FeatureFlagsNamespace(FeatureFlags): """FeatureFlags resources.""" @@ -259,6 +272,17 @@ def __init__(self, client: "AsyncWorkOSClient") -> None: def challenges(self) -> AsyncMultiFactorAuthChallenges: return AsyncMultiFactorAuthChallenges(self._client) + async def verify_challenge( + self, + *, + authentication_challenge_id: str, + code: str, + request_options: Optional[RequestOptions] = None, + ) -> Any: + return await self.challenges.verify( + authentication_challenge_id, code=code, request_options=request_options + ) + class AsyncFeatureFlagsNamespace(AsyncFeatureFlags): """FeatureFlags resources (async).""" @@ -513,14 +537,25 @@ def _raise_error(response: httpx.Response) -> None: request = response.request request_url = str(request.url) if request is not None else None request_method = request.method if request is not None else None + response_json: Optional[Dict[str, Any]] = None try: - body: Dict[str, Any] = response.json() - message: str = str(body.get("message", response.text)) - code: Optional[str] = str(body["code"]) if "code" in body else None - param = cast(Optional[str], body.get("param")) + response_json = cast(Dict[str, Any], response.json()) + message: str = str(response_json.get("message", response.text)) + error = cast(Optional[str], response_json.get("error")) + errors = response_json.get("errors") + code: Optional[str] = ( + str(response_json["code"]) if "code" in response_json else None + ) + error_description = cast( + Optional[str], response_json.get("error_description") + ) + param = cast(Optional[str], response_json.get("param")) except Exception: message = response.text + error = None + errors = None code = None + error_description = None param = None error_class = STATUS_CODE_TO_EXCEPTION.get(response.status_code) @@ -535,6 +570,11 @@ def _raise_error(response: httpx.Response) -> None: request_id=request_id, code=code, param=param, + response=response, + response_json=response_json, + error=error, + errors=errors, + error_description=error_description, raw_body=raw_body, request_url=request_url, request_method=request_method, @@ -544,6 +584,11 @@ def _raise_error(response: httpx.Response) -> None: request_id=request_id, code=code, param=param, + response=response, + response_json=response_json, + error=error, + errors=errors, + error_description=error_description, raw_body=raw_body, request_url=request_url, request_method=request_method, @@ -556,6 +601,11 @@ def _raise_error(response: httpx.Response) -> None: request_id=request_id, code=code, param=param, + response=response, + response_json=response_json, + error=error, + errors=errors, + error_description=error_description, raw_body=raw_body, request_url=request_url, request_method=request_method, @@ -567,6 +617,11 @@ def _raise_error(response: httpx.Response) -> None: request_id=request_id, code=code, param=param, + response=response, + response_json=response_json, + error=error, + errors=errors, + error_description=error_description, raw_body=raw_body, request_url=request_url, request_method=request_method, @@ -717,6 +772,18 @@ def user_management(self) -> UserManagementNamespace: def user_management_users(self) -> UserManagementUsersNamespace: return UserManagementUsersNamespace(self) + @functools.cached_property + def mfa(self) -> Any: + return self.multi_factor_auth + + @functools.cached_property + def passwordless(self) -> Passwordless: + return Passwordless(self) + + @functools.cached_property + def vault(self) -> Vault: + return Vault(self) + @overload def request( self, @@ -984,6 +1051,18 @@ def user_management(self) -> AsyncUserManagementNamespace: def user_management_users(self) -> AsyncUserManagementUsersNamespace: return AsyncUserManagementUsersNamespace(self) + @functools.cached_property + def mfa(self) -> Any: + return self.multi_factor_auth + + @functools.cached_property + def passwordless(self) -> AsyncPasswordless: + return AsyncPasswordless(self) + + @functools.cached_property + def vault(self) -> AsyncVault: + return AsyncVault(self) + @overload async def request( self, diff --git a/src/workos/_errors.py b/src/workos/_errors.py index 79da3c1b..55a3b53f 100644 --- a/src/workos/_errors.py +++ b/src/workos/_errors.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Dict, Optional, Type +from typing import Any, Dict, Mapping, Optional, Type, cast class BaseRequestException(Exception): @@ -16,11 +16,15 @@ class BaseRequestException(Exception): raw_body: Optional[str] request_url: Optional[str] request_method: Optional[str] + response: Optional[Any] + response_json: Optional[Mapping[str, Any]] + error: Optional[str] + errors: Optional[Any] + error_description: Optional[str] def __init__( self, - message: str, - *, + *args: Any, status_code: Optional[int] = None, request_id: Optional[str] = None, code: Optional[str] = None, @@ -28,7 +32,55 @@ def __init__( raw_body: Optional[str] = None, request_url: Optional[str] = None, request_method: Optional[str] = None, + response: Optional[Any] = None, + response_json: Optional[Mapping[str, Any]] = None, + error: Optional[str] = None, + errors: Optional[Any] = None, + error_description: Optional[str] = None, ) -> None: + message: Optional[str] = None + if args: + first = args[0] + if isinstance(first, str): + message = first + else: + response = first + if len(args) > 1: + response_json = cast(Optional[Mapping[str, Any]], args[1]) + if response is not None and status_code is None: + status_code = cast(Optional[int], getattr(response, "status_code", None)) + headers = getattr(response, "headers", None) + if request_id is None and headers is not None: + request_id = headers.get("X-Request-ID") or headers.get("x-request-id") + if ( + request_url is None + and response is not None + and getattr(response, "request", None) is not None + ): + request_url = str(response.request.url) + if ( + request_method is None + and response is not None + and getattr(response, "request", None) is not None + ): + request_method = response.request.method + if response_json is not None: + if message is None: + message = str(response_json.get("message", "No message")) + if error is None: + error = cast(Optional[str], response_json.get("error")) + if errors is None: + errors = response_json.get("errors") + if code is None: + code = cast(Optional[str], response_json.get("code")) + if error_description is None: + error_description = cast( + Optional[str], response_json.get("error_description") + ) + if param is None: + param = cast(Optional[str], response_json.get("param")) + if message is None: + message = "No message" super().__init__(message) self.message = message self.status_code = status_code @@ -38,84 +90,45 @@ def __init__( self.raw_body = raw_body self.request_url = request_url self.request_method = request_method + self.response = response + self.response_json = response_json + self.error = error + self.errors = errors + self.error_description = error_description + + def __str__(self) -> str: + exception = f"(message={self.message}, request_id={self.request_id}" + if self.response_json is not None: + for key, value in self.response_json.items(): + if key != "message": + exception += f", {key}={value}" + elif self.code is not None: + exception += f", code={self.code}" + return exception + ")" class BadRequestException(BaseRequestException): """400 Bad Request.""" - def __init__( - self, - message: str = "Bad request", - *, - request_id: Optional[str] = None, - code: Optional[str] = None, - param: Optional[str] = None, - raw_body: Optional[str] = None, - request_url: Optional[str] = None, - request_method: Optional[str] = None, - ) -> None: - super().__init__( - message, - status_code=400, - request_id=request_id, - code=code, - param=param, - raw_body=raw_body, - request_url=request_url, - request_method=request_method, - ) + def __init__(self, *args: Any, **kwargs: Any) -> None: + kwargs.setdefault("status_code", 400) + super().__init__(*args, **kwargs) class AuthenticationException(BaseRequestException): """401 Unauthorized.""" - def __init__( - self, - message: str = "Unauthorized", - *, - request_id: Optional[str] = None, - code: Optional[str] = None, - param: Optional[str] = None, - raw_body: Optional[str] = None, - request_url: Optional[str] = None, - request_method: Optional[str] = None, - ) -> None: - super().__init__( - message, - status_code=401, - request_id=request_id, - code=code, - param=param, - raw_body=raw_body, - request_url=request_url, - request_method=request_method, - ) + def __init__(self, *args: Any, **kwargs: Any) -> None: + kwargs.setdefault("status_code", 401) + super().__init__(*args, **kwargs) class AuthorizationException(BaseRequestException): """403 Forbidden.""" - def __init__( - self, - message: str = "Forbidden", - *, - request_id: Optional[str] = None, - code: Optional[str] = None, - param: Optional[str] = None, - raw_body: Optional[str] = None, - request_url: Optional[str] = None, - request_method: Optional[str] = None, - ) -> None: - super().__init__( - message, - status_code=403, - request_id=request_id, - code=code, - param=param, - raw_body=raw_body, - request_url=request_url, - request_method=request_method, - ) + def __init__(self, *args: Any, **kwargs: Any) -> None: + kwargs.setdefault("status_code", 403) + super().__init__(*args, **kwargs) class EmailVerificationRequiredException(AuthorizationException): @@ -124,105 +137,39 @@ class EmailVerificationRequiredException(AuthorizationException): email_verification_id: Optional[str] def __init__( - self, - message: str = "Email verification required", - *, - email_verification_id: Optional[str] = None, - request_id: Optional[str] = None, - code: Optional[str] = None, - param: Optional[str] = None, - raw_body: Optional[str] = None, - request_url: Optional[str] = None, - request_method: Optional[str] = None, + self, *args: Any, email_verification_id: Optional[str] = None, **kwargs: Any ) -> None: - super().__init__( - message, - request_id=request_id, - code=code, - param=param, - raw_body=raw_body, - request_url=request_url, - request_method=request_method, - ) + response_json = cast(Optional[Mapping[str, Any]], kwargs.get("response_json")) + if email_verification_id is None and response_json is not None: + email_verification_id = cast( + Optional[str], response_json.get("email_verification_id") + ) + super().__init__(*args, **kwargs) self.email_verification_id = email_verification_id class NotFoundException(BaseRequestException): """404 Not Found.""" - def __init__( - self, - message: str = "Not found", - *, - request_id: Optional[str] = None, - code: Optional[str] = None, - param: Optional[str] = None, - raw_body: Optional[str] = None, - request_url: Optional[str] = None, - request_method: Optional[str] = None, - ) -> None: - super().__init__( - message, - status_code=404, - request_id=request_id, - code=code, - param=param, - raw_body=raw_body, - request_url=request_url, - request_method=request_method, - ) + def __init__(self, *args: Any, **kwargs: Any) -> None: + kwargs.setdefault("status_code", 404) + super().__init__(*args, **kwargs) class ConflictException(BaseRequestException): """409 Conflict.""" - def __init__( - self, - message: str = "Conflict", - *, - request_id: Optional[str] = None, - code: Optional[str] = None, - param: Optional[str] = None, - raw_body: Optional[str] = None, - request_url: Optional[str] = None, - request_method: Optional[str] = None, - ) -> None: - super().__init__( - message, - status_code=409, - request_id=request_id, - code=code, - param=param, - raw_body=raw_body, - request_url=request_url, - request_method=request_method, - ) + def __init__(self, *args: Any, **kwargs: Any) -> None: + kwargs.setdefault("status_code", 409) + super().__init__(*args, **kwargs) class UnprocessableEntityException(BaseRequestException): """422 Unprocessable Entity.""" - def __init__( - self, - message: str = "Unprocessable entity", - *, - request_id: Optional[str] = None, - code: Optional[str] = None, - param: Optional[str] = None, - raw_body: Optional[str] = None, - request_url: Optional[str] = None, - request_method: Optional[str] = None, - ) -> None: - super().__init__( - message, - status_code=422, - request_id=request_id, - code=code, - param=param, - raw_body=raw_body, - request_url=request_url, - request_method=request_method, - ) + def __init__(self, *args: Any, **kwargs: Any) -> None: + kwargs.setdefault("status_code", 422) + super().__init__(*args, **kwargs) class RateLimitExceededException(BaseRequestException): @@ -231,55 +178,19 @@ class RateLimitExceededException(BaseRequestException): retry_after: Optional[float] def __init__( - self, - message: str = "Too many requests", - *, - retry_after: Optional[float] = None, - request_id: Optional[str] = None, - code: Optional[str] = None, - param: Optional[str] = None, - raw_body: Optional[str] = None, - request_url: Optional[str] = None, - request_method: Optional[str] = None, + self, *args: Any, retry_after: Optional[float] = None, **kwargs: Any ) -> None: - super().__init__( - message, - status_code=429, - request_id=request_id, - code=code, - param=param, - raw_body=raw_body, - request_url=request_url, - request_method=request_method, - ) + kwargs.setdefault("status_code", 429) + super().__init__(*args, **kwargs) self.retry_after = retry_after class ServerException(BaseRequestException): """500+ Server Error.""" - def __init__( - self, - message: str = "Server error", - *, - status_code: int = 500, - request_id: Optional[str] = None, - code: Optional[str] = None, - param: Optional[str] = None, - raw_body: Optional[str] = None, - request_url: Optional[str] = None, - request_method: Optional[str] = None, - ) -> None: - super().__init__( - message, - status_code=status_code, - request_id=request_id, - code=code, - param=param, - raw_body=raw_body, - request_url=request_url, - request_method=request_method, - ) + def __init__(self, *args: Any, **kwargs: Any) -> None: + kwargs.setdefault("status_code", 500) + super().__init__(*args, **kwargs) class ConfigurationException(BaseRequestException): diff --git a/src/workos/_types.py b/src/workos/_types.py index 499d0e52..3b84d590 100644 --- a/src/workos/_types.py +++ b/src/workos/_types.py @@ -2,6 +2,7 @@ from __future__ import annotations +from enum import Enum from typing import Any, Dict, Protocol, TypeVar from typing_extensions import Self, TypedDict @@ -23,4 +24,9 @@ class Deserializable(Protocol): def from_dict(cls, data: Dict[str, Any]) -> Self: ... +def enum_value(value: Any) -> Any: + """Serialize enum-like values without rejecting raw string inputs.""" + return value.value if isinstance(value, Enum) else value + + D = TypeVar("D", bound=Deserializable) diff --git a/src/workos/admin_portal/_resource.py b/src/workos/admin_portal/_resource.py index 0082d2f7..9012fca8 100644 --- a/src/workos/admin_portal/_resource.py +++ b/src/workos/admin_portal/_resource.py @@ -2,14 +2,14 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import TYPE_CHECKING, Any, Dict, Optional, Union if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient +from .._types import RequestOptions, enum_value from .models import IntentOptions, PortalLinkResponse from workos.common.models import GenerateLinkDtoIntent -from .._types import RequestOptions class AdminPortal: @@ -18,13 +18,13 @@ class AdminPortal: def __init__(self, client: "WorkOSClient") -> None: self._client = client - def create( + def generate_link( self, *, organization: str, return_url: Optional[str] = None, success_url: Optional[str] = None, - intent: Optional[GenerateLinkDtoIntent] = None, + intent: Optional[Union[GenerateLinkDtoIntent, str]] = None, intent_options: Optional[IntentOptions] = None, request_options: Optional[RequestOptions] = None, ) -> PortalLinkResponse: @@ -66,7 +66,7 @@ def create( "return_url": return_url, "success_url": success_url, "organization": organization, - "intent": intent, + "intent": enum_value(intent) if intent is not None else None, "intent_options": intent_options.to_dict() if intent_options is not None else None, @@ -81,6 +81,10 @@ def create( request_options=request_options, ) + def create(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `generate_link`.""" + return self.generate_link(*args, **kwargs) + class AsyncAdminPortal: """Admin Portal API resources (async).""" @@ -88,13 +92,13 @@ class AsyncAdminPortal: def __init__(self, client: "AsyncWorkOSClient") -> None: self._client = client - async def create( + async def generate_link( self, *, organization: str, return_url: Optional[str] = None, success_url: Optional[str] = None, - intent: Optional[GenerateLinkDtoIntent] = None, + intent: Optional[Union[GenerateLinkDtoIntent, str]] = None, intent_options: Optional[IntentOptions] = None, request_options: Optional[RequestOptions] = None, ) -> PortalLinkResponse: @@ -136,7 +140,7 @@ async def create( "return_url": return_url, "success_url": success_url, "organization": organization, - "intent": intent, + "intent": enum_value(intent) if intent is not None else None, "intent_options": intent_options.to_dict() if intent_options is not None else None, @@ -150,3 +154,7 @@ async def create( model=PortalLinkResponse, request_options=request_options, ) + + async def create(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `generate_link`.""" + return await self.generate_link(*args, **kwargs) diff --git a/src/workos/api_keys/_resource.py b/src/workos/api_keys/_resource.py index 68950b58..70b0140c 100644 --- a/src/workos/api_keys/_resource.py +++ b/src/workos/api_keys/_resource.py @@ -7,8 +7,8 @@ if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient -from .models import ApiKeyValidationResponse from .._types import RequestOptions +from .models import ApiKeyValidationResponse class ApiKeys: diff --git a/src/workos/application_client_secrets/_resource.py b/src/workos/application_client_secrets/_resource.py index 0f981d56..01e6a897 100644 --- a/src/workos/application_client_secrets/_resource.py +++ b/src/workos/application_client_secrets/_resource.py @@ -7,8 +7,8 @@ if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient -from .models import ApplicationCredentialsListItem, NewConnectApplicationSecret from .._types import RequestOptions +from .models import ApplicationCredentialsListItem, NewConnectApplicationSecret class ApplicationClientSecrets: diff --git a/src/workos/applications/_resource.py b/src/workos/applications/_resource.py index 629efd78..8d3275c8 100644 --- a/src/workos/applications/_resource.py +++ b/src/workos/applications/_resource.py @@ -7,6 +7,7 @@ if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient +from .._types import RequestOptions, enum_value from .models import ( ConnectApplication, CreateM2MApplication, @@ -15,7 +16,6 @@ ) from .models import ApplicationsOrder from .._pagination import AsyncPage, SyncPage -from .._types import RequestOptions class Applications: @@ -30,7 +30,7 @@ def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[ApplicationsOrder] = None, + order: Optional[Union[ApplicationsOrder, str]] = None, organization_id: Optional[str] = None, request_options: Optional[RequestOptions] = None, ) -> SyncPage[ConnectApplication]: @@ -61,7 +61,7 @@ def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "organization_id": organization_id, }.items() if v is not None @@ -228,7 +228,7 @@ async def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[ApplicationsOrder] = None, + order: Optional[Union[ApplicationsOrder, str]] = None, organization_id: Optional[str] = None, request_options: Optional[RequestOptions] = None, ) -> AsyncPage[ConnectApplication]: @@ -259,7 +259,7 @@ async def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "organization_id": organization_id, }.items() if v is not None diff --git a/src/workos/audit_logs/_resource.py b/src/workos/audit_logs/_resource.py index 28900a36..b45a046c 100644 --- a/src/workos/audit_logs/_resource.py +++ b/src/workos/audit_logs/_resource.py @@ -2,11 +2,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient +from .._types import RequestOptions, enum_value from .models import ( AuditLogActionJson, AuditLogEventCreateResponse, @@ -18,7 +19,6 @@ ) from .models import AuditLogsOrder from .._pagination import AsyncPage, SyncPage -from .._types import RequestOptions class AuditLogs: @@ -33,7 +33,7 @@ def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[AuditLogsOrder] = None, + order: Optional[Union[AuditLogsOrder, str]] = None, request_options: Optional[RequestOptions] = None, ) -> SyncPage[AuditLogActionJson]: """List Actions @@ -63,7 +63,7 @@ def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } @@ -75,14 +75,14 @@ def list( request_options=request_options, ) - def list_schemas( + def schemas( self, action_name: str, *, limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[AuditLogsOrder] = None, + order: Optional[Union[AuditLogsOrder, str]] = None, request_options: Optional[RequestOptions] = None, ) -> SyncPage[AuditLogSchemaJson]: """List Schemas @@ -113,7 +113,7 @@ def list_schemas( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } @@ -125,7 +125,7 @@ def list_schemas( request_options=request_options, ) - def create_schema( + def create_schemas( self, action_name: str, *, @@ -171,7 +171,7 @@ def create_schema( request_options=request_options, ) - def create_event( + def create_events( self, *, organization_id: str, @@ -321,7 +321,7 @@ async def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[AuditLogsOrder] = None, + order: Optional[Union[AuditLogsOrder, str]] = None, request_options: Optional[RequestOptions] = None, ) -> AsyncPage[AuditLogActionJson]: """List Actions @@ -351,7 +351,7 @@ async def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } @@ -363,14 +363,14 @@ async def list( request_options=request_options, ) - async def list_schemas( + async def schemas( self, action_name: str, *, limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[AuditLogsOrder] = None, + order: Optional[Union[AuditLogsOrder, str]] = None, request_options: Optional[RequestOptions] = None, ) -> AsyncPage[AuditLogSchemaJson]: """List Schemas @@ -401,7 +401,7 @@ async def list_schemas( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } @@ -413,7 +413,7 @@ async def list_schemas( request_options=request_options, ) - async def create_schema( + async def create_schemas( self, action_name: str, *, @@ -459,7 +459,7 @@ async def create_schema( request_options=request_options, ) - async def create_event( + async def create_events( self, *, organization_id: str, diff --git a/src/workos/audit_logs/models/__init__.py b/src/workos/audit_logs/models/__init__.py index 5f15d1e7..a57f4c77 100644 --- a/src/workos/audit_logs/models/__init__.py +++ b/src/workos/audit_logs/models/__init__.py @@ -1,5 +1,8 @@ # This file is auto-generated by oagen. Do not edit. +from .applications_order_literal import ( + ApplicationsOrderLiteral as ApplicationsOrderLiteral, +) from .audit_log_action_json import AuditLogActionJson as AuditLogActionJson from .audit_log_event_actor import AuditLogEventActor as AuditLogEventActor from .audit_log_event_context import AuditLogEventContext as AuditLogEventContext diff --git a/src/workos/audit_logs/models/applications_order_literal.py b/src/workos/audit_logs/models/applications_order_literal.py new file mode 100644 index 00000000..210344b4 --- /dev/null +++ b/src/workos/audit_logs/models/applications_order_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import ApplicationsOrder + +ApplicationsOrderLiteral = ApplicationsOrder +__all__ = ["ApplicationsOrderLiteral"] diff --git a/src/workos/authorization/_resource.py b/src/workos/authorization/_resource.py index d57769e2..aab88013 100644 --- a/src/workos/authorization/_resource.py +++ b/src/workos/authorization/_resource.py @@ -2,11 +2,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient +from .._types import RequestOptions, enum_value from .models import ( AuthorizationCheck, AuthorizationResource, @@ -18,7 +19,6 @@ ) from .models import AuthorizationAssignment, AuthorizationOrder from .._pagination import AsyncPage, SyncPage -from .._types import RequestOptions class Authorization: @@ -85,7 +85,7 @@ def list_resources_for_membership( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[AuthorizationOrder] = None, + order: Optional[Union[AuthorizationOrder, str]] = None, permission_slug: str, parent_resource_id: Optional[str] = None, parent_resource_type_slug: Optional[str] = None, @@ -128,7 +128,7 @@ def list_resources_for_membership( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "permission_slug": permission_slug, "parent_resource_id": parent_resource_id, "parent_resource_type_slug": parent_resource_type_slug, @@ -151,7 +151,7 @@ def list_role_assignments( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[AuthorizationOrder] = None, + order: Optional[Union[AuthorizationOrder, str]] = None, request_options: Optional[RequestOptions] = None, ) -> SyncPage[RoleAssignment]: """List role assignments @@ -182,7 +182,7 @@ def list_role_assignments( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } @@ -245,7 +245,7 @@ def assign_role( request_options=request_options, ) - def remove_role( + def remove_role_by_criteria( self, organization_membership_id: str, *, @@ -321,7 +321,7 @@ def remove_role_by_id( request_options=request_options, ) - def list_roles_organizations( + def list_organization_roles( self, organization_id: str, *, @@ -352,7 +352,11 @@ def list_roles_organizations( request_options=request_options, ) - def create_roles_organizations( + def list_roles_organizations(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `list_organization_roles`.""" + return self.list_organization_roles(*args, **kwargs) + + def create_organization_role( self, organization_id: str, *, @@ -402,7 +406,11 @@ def create_roles_organizations( request_options=request_options, ) - def get_roles_organization( + def create_roles_organizations(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `create_organization_role`.""" + return self.create_organization_role(*args, **kwargs) + + def get_organization_role( self, organization_id: str, slug: str, @@ -435,7 +443,11 @@ def get_roles_organization( request_options=request_options, ) - def update_roles_organizations( + def get_roles_organization(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `get_organization_role`.""" + return self.get_organization_role(*args, **kwargs) + + def update_organization_role( self, organization_id: str, slug: str, @@ -483,7 +495,11 @@ def update_roles_organizations( request_options=request_options, ) - def delete_roles( + def update_roles_organizations(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `update_organization_role`.""" + return self.update_organization_role(*args, **kwargs) + + def delete_organization_role( self, organization_id: str, slug: str, @@ -514,6 +530,10 @@ def delete_roles( request_options=request_options, ) + def delete_roles(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `delete_organization_role`.""" + return self.delete_organization_role(*args, **kwargs) + def add_permission_permissions_organizations_roles( self, organization_id: str, @@ -773,9 +793,9 @@ def list_organization_memberships_for_resource_by_external_id( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[AuthorizationOrder] = None, + order: Optional[Union[AuthorizationOrder, str]] = None, permission_slug: str, - assignment: Optional[AuthorizationAssignment] = None, + assignment: Optional[Union[AuthorizationAssignment, str]] = None, request_options: Optional[RequestOptions] = None, ) -> SyncPage[UserOrganizationMembershipBaseListData]: """List memberships for a resource by external ID @@ -812,9 +832,11 @@ def list_organization_memberships_for_resource_by_external_id( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "permission_slug": permission_slug, - "assignment": assignment.value if assignment else None, + "assignment": enum_value(assignment) + if assignment is not None + else None, }.items() if v is not None } @@ -832,7 +854,7 @@ def list_resources( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[AuthorizationOrder] = None, + order: Optional[Union[AuthorizationOrder, str]] = None, organization_id: Optional[str] = None, resource_type_slug: Optional[str] = None, parent_resource_id: Optional[str] = None, @@ -874,7 +896,7 @@ def list_resources( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "organization_id": organization_id, "resource_type_slug": resource_type_slug, "parent_resource_id": parent_resource_id, @@ -892,7 +914,7 @@ def list_resources( request_options=request_options, ) - def create_resource( + def create_resources( self, *, external_id: str, @@ -987,7 +1009,7 @@ def get_by_id( request_options=request_options, ) - def update_resource( + def update_resources( self, resource_id: str, *, @@ -1043,7 +1065,7 @@ def update_resource( request_options=request_options, ) - def delete_resource( + def delete_resources( self, resource_id: str, *, @@ -1089,9 +1111,9 @@ def list_organization_memberships_for_resource( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[AuthorizationOrder] = None, + order: Optional[Union[AuthorizationOrder, str]] = None, permission_slug: str, - assignment: Optional[AuthorizationAssignment] = None, + assignment: Optional[Union[AuthorizationAssignment, str]] = None, request_options: Optional[RequestOptions] = None, ) -> SyncPage[UserOrganizationMembershipBaseListData]: """List organization memberships for resource @@ -1126,9 +1148,11 @@ def list_organization_memberships_for_resource( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "permission_slug": permission_slug, - "assignment": assignment.value if assignment else None, + "assignment": enum_value(assignment) + if assignment is not None + else None, }.items() if v is not None } @@ -1436,7 +1460,7 @@ async def list_resources_for_membership( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[AuthorizationOrder] = None, + order: Optional[Union[AuthorizationOrder, str]] = None, permission_slug: str, parent_resource_id: Optional[str] = None, parent_resource_type_slug: Optional[str] = None, @@ -1479,7 +1503,7 @@ async def list_resources_for_membership( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "permission_slug": permission_slug, "parent_resource_id": parent_resource_id, "parent_resource_type_slug": parent_resource_type_slug, @@ -1502,7 +1526,7 @@ async def list_role_assignments( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[AuthorizationOrder] = None, + order: Optional[Union[AuthorizationOrder, str]] = None, request_options: Optional[RequestOptions] = None, ) -> AsyncPage[RoleAssignment]: """List role assignments @@ -1533,7 +1557,7 @@ async def list_role_assignments( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } @@ -1596,7 +1620,7 @@ async def assign_role( request_options=request_options, ) - async def remove_role( + async def remove_role_by_criteria( self, organization_membership_id: str, *, @@ -1672,7 +1696,7 @@ async def remove_role_by_id( request_options=request_options, ) - async def list_roles_organizations( + async def list_organization_roles( self, organization_id: str, *, @@ -1703,7 +1727,11 @@ async def list_roles_organizations( request_options=request_options, ) - async def create_roles_organizations( + async def list_roles_organizations(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `list_organization_roles`.""" + return await self.list_organization_roles(*args, **kwargs) + + async def create_organization_role( self, organization_id: str, *, @@ -1753,7 +1781,11 @@ async def create_roles_organizations( request_options=request_options, ) - async def get_roles_organization( + async def create_roles_organizations(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `create_organization_role`.""" + return await self.create_organization_role(*args, **kwargs) + + async def get_organization_role( self, organization_id: str, slug: str, @@ -1786,7 +1818,11 @@ async def get_roles_organization( request_options=request_options, ) - async def update_roles_organizations( + async def get_roles_organization(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `get_organization_role`.""" + return await self.get_organization_role(*args, **kwargs) + + async def update_organization_role( self, organization_id: str, slug: str, @@ -1834,7 +1870,11 @@ async def update_roles_organizations( request_options=request_options, ) - async def delete_roles( + async def update_roles_organizations(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `update_organization_role`.""" + return await self.update_organization_role(*args, **kwargs) + + async def delete_organization_role( self, organization_id: str, slug: str, @@ -1865,6 +1905,10 @@ async def delete_roles( request_options=request_options, ) + async def delete_roles(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `delete_organization_role`.""" + return await self.delete_organization_role(*args, **kwargs) + async def add_permission_permissions_organizations_roles( self, organization_id: str, @@ -2124,9 +2168,9 @@ async def list_organization_memberships_for_resource_by_external_id( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[AuthorizationOrder] = None, + order: Optional[Union[AuthorizationOrder, str]] = None, permission_slug: str, - assignment: Optional[AuthorizationAssignment] = None, + assignment: Optional[Union[AuthorizationAssignment, str]] = None, request_options: Optional[RequestOptions] = None, ) -> AsyncPage[UserOrganizationMembershipBaseListData]: """List memberships for a resource by external ID @@ -2163,9 +2207,11 @@ async def list_organization_memberships_for_resource_by_external_id( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "permission_slug": permission_slug, - "assignment": assignment.value if assignment else None, + "assignment": enum_value(assignment) + if assignment is not None + else None, }.items() if v is not None } @@ -2183,7 +2229,7 @@ async def list_resources( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[AuthorizationOrder] = None, + order: Optional[Union[AuthorizationOrder, str]] = None, organization_id: Optional[str] = None, resource_type_slug: Optional[str] = None, parent_resource_id: Optional[str] = None, @@ -2225,7 +2271,7 @@ async def list_resources( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "organization_id": organization_id, "resource_type_slug": resource_type_slug, "parent_resource_id": parent_resource_id, @@ -2243,7 +2289,7 @@ async def list_resources( request_options=request_options, ) - async def create_resource( + async def create_resources( self, *, external_id: str, @@ -2338,7 +2384,7 @@ async def get_by_id( request_options=request_options, ) - async def update_resource( + async def update_resources( self, resource_id: str, *, @@ -2394,7 +2440,7 @@ async def update_resource( request_options=request_options, ) - async def delete_resource( + async def delete_resources( self, resource_id: str, *, @@ -2440,9 +2486,9 @@ async def list_organization_memberships_for_resource( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[AuthorizationOrder] = None, + order: Optional[Union[AuthorizationOrder, str]] = None, permission_slug: str, - assignment: Optional[AuthorizationAssignment] = None, + assignment: Optional[Union[AuthorizationAssignment, str]] = None, request_options: Optional[RequestOptions] = None, ) -> AsyncPage[UserOrganizationMembershipBaseListData]: """List organization memberships for resource @@ -2477,9 +2523,11 @@ async def list_organization_memberships_for_resource( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "permission_slug": permission_slug, - "assignment": assignment.value if assignment else None, + "assignment": enum_value(assignment) + if assignment is not None + else None, }.items() if v is not None } diff --git a/src/workos/authorization/models/__init__.py b/src/workos/authorization/models/__init__.py index dde98d68..162d7f0b 100644 --- a/src/workos/authorization/models/__init__.py +++ b/src/workos/authorization/models/__init__.py @@ -2,8 +2,10 @@ from .add_role_permission import AddRolePermission as AddRolePermission from .assign_role import AssignRole as AssignRole -from .assignment import Assignment as Assignment from .authorization_assignment import AuthorizationAssignment as AuthorizationAssignment +from .authorization_assignment_literal import ( + AuthorizationAssignmentLiteral as AuthorizationAssignmentLiteral, +) from .authorization_check import AuthorizationCheck as AuthorizationCheck from .authorization_order import AuthorizationOrder as AuthorizationOrder from .authorization_resource import AuthorizationResource as AuthorizationResource diff --git a/src/workos/authorization/models/authorization_assignment_literal.py b/src/workos/authorization/models/authorization_assignment_literal.py new file mode 100644 index 00000000..4c86f98c --- /dev/null +++ b/src/workos/authorization/models/authorization_assignment_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .authorization_assignment import AuthorizationAssignment + +AuthorizationAssignmentLiteral = AuthorizationAssignment +__all__ = ["AuthorizationAssignmentLiteral"] diff --git a/src/workos/common/__init__.py b/src/workos/common/__init__.py index beae7f53..178853fe 100644 --- a/src/workos/common/__init__.py +++ b/src/workos/common/__init__.py @@ -3,79 +3,149 @@ from .models import ( AuditLogConfigurationLogStreamState as AuditLogConfigurationLogStreamState, ) +from .models import ( + AuditLogConfigurationLogStreamStateLiteral as AuditLogConfigurationLogStreamStateLiteral, +) from .models import ( AuditLogConfigurationLogStreamType as AuditLogConfigurationLogStreamType, ) +from .models import ( + AuditLogConfigurationLogStreamTypeLiteral as AuditLogConfigurationLogStreamTypeLiteral, +) from .models import AuditLogConfigurationState as AuditLogConfigurationState +from .models import ( + AuditLogConfigurationStateLiteral as AuditLogConfigurationStateLiteral, +) from .models import AuditLogExportJsonState as AuditLogExportJsonState -from .models import AuditLogExportState as AuditLogExportState -from .models import AuditLogStreamState as AuditLogStreamState -from .models import AuditLogTrailState as AuditLogTrailState -from .models import AuthMethodType as AuthMethodType +from .models import AuditLogExportJsonStateLiteral as AuditLogExportJsonStateLiteral from .models import ( AuthenticateResponseAuthenticationMethod as AuthenticateResponseAuthenticationMethod, ) +from .models import ( + AuthenticateResponseAuthenticationMethodLiteral as AuthenticateResponseAuthenticationMethodLiteral, +) from .models import AuthenticationFactorEnrolledType as AuthenticationFactorEnrolledType +from .models import ( + AuthenticationFactorEnrolledTypeLiteral as AuthenticationFactorEnrolledTypeLiteral, +) from .models import AuthenticationFactorType as AuthenticationFactorType from .models import ( AuthenticationFactorsCreateRequestType as AuthenticationFactorsCreateRequestType, ) +from .models import ( + AuthenticationFactorsCreateRequestTypeLiteral as AuthenticationFactorsCreateRequestTypeLiteral, +) from .models import ConnectedAccountState as ConnectedAccountState +from .models import ConnectedAccountStateLiteral as ConnectedAccountStateLiteral from .models import ConnectionState as ConnectionState +from .models import ConnectionStateLiteral as ConnectionStateLiteral from .models import ConnectionStatus as ConnectionStatus +from .models import ConnectionStatusLiteral as ConnectionStatusLiteral from .models import ConnectionType as ConnectionType +from .models import ConnectionTypeLiteral as ConnectionTypeLiteral from .models import CreateUserDtoPasswordHashType as CreateUserDtoPasswordHashType +from .models import ( + CreateUserDtoPasswordHashTypeLiteral as CreateUserDtoPasswordHashTypeLiteral, +) from .models import CreateUserInviteOptionsDtoLocale as CreateUserInviteOptionsDtoLocale +from .models import ( + CreateUserInviteOptionsDtoLocaleLiteral as CreateUserInviteOptionsDtoLocaleLiteral, +) from .models import CreateWebhookEndpointDtoEvents as CreateWebhookEndpointDtoEvents +from .models import ( + CreateWebhookEndpointDtoEventsLiteral as CreateWebhookEndpointDtoEventsLiteral, +) from .models import ( DataIntegrationsListResponseDataConnectedAccountState as DataIntegrationsListResponseDataConnectedAccountState, ) from .models import ( DataIntegrationsListResponseDataOwnership as DataIntegrationsListResponseDataOwnership, ) +from .models import ( + DataIntegrationsListResponseDataOwnershipLiteral as DataIntegrationsListResponseDataOwnershipLiteral, +) from .models import DirectoryState as DirectoryState +from .models import DirectoryStateLiteral as DirectoryStateLiteral from .models import DirectoryType as DirectoryType +from .models import DirectoryTypeLiteral as DirectoryTypeLiteral from .models import DirectoryUserWithGroupsState as DirectoryUserWithGroupsState +from .models import ( + DirectoryUserWithGroupsStateLiteral as DirectoryUserWithGroupsStateLiteral, +) from .models import GenerateLinkDtoIntent as GenerateLinkDtoIntent +from .models import GenerateLinkDtoIntentLiteral as GenerateLinkDtoIntentLiteral from .models import InvitationState as InvitationState +from .models import InvitationStateLiteral as InvitationStateLiteral from .models import ListDataType as ListDataType +from .models import ListDataTypeLiteral as ListDataTypeLiteral from .models import OrganizationDomainDataDtoState as OrganizationDomainDataDtoState +from .models import ( + OrganizationDomainDataDtoStateLiteral as OrganizationDomainDataDtoStateLiteral, +) from .models import ( OrganizationDomainStandAloneState as OrganizationDomainStandAloneState, ) +from .models import ( + OrganizationDomainStandAloneStateLiteral as OrganizationDomainStandAloneStateLiteral, +) from .models import ( OrganizationDomainStandAloneVerificationStrategy as OrganizationDomainStandAloneVerificationStrategy, ) +from .models import ( + OrganizationDomainStandAloneVerificationStrategyLiteral as OrganizationDomainStandAloneVerificationStrategyLiteral, +) from .models import OrganizationDomainState as OrganizationDomainState from .models import ( OrganizationDomainVerificationStrategy as OrganizationDomainVerificationStrategy, ) from .models import OrganizationMembershipStatus as OrganizationMembershipStatus -from .models import PasswordHashType as PasswordHashType from .models import ProfileConnectionType as ProfileConnectionType from .models import ( RadarStandaloneAssessRequestAction as RadarStandaloneAssessRequestAction, ) +from .models import ( + RadarStandaloneAssessRequestActionLiteral as RadarStandaloneAssessRequestActionLiteral, +) from .models import ( RadarStandaloneAssessRequestAuthMethod as RadarStandaloneAssessRequestAuthMethod, ) +from .models import ( + RadarStandaloneAssessRequestAuthMethodLiteral as RadarStandaloneAssessRequestAuthMethodLiteral, +) from .models import ( RadarStandaloneResponseBlocklistType as RadarStandaloneResponseBlocklistType, ) from .models import RadarStandaloneResponseControl as RadarStandaloneResponseControl +from .models import ( + RadarStandaloneResponseControlLiteral as RadarStandaloneResponseControlLiteral, +) from .models import RadarStandaloneResponseVerdict as RadarStandaloneResponseVerdict +from .models import ( + RadarStandaloneResponseVerdictLiteral as RadarStandaloneResponseVerdictLiteral, +) from .models import ResendUserInviteOptionsDtoLocale as ResendUserInviteOptionsDtoLocale from .models import RoleType as RoleType from .models import UpdateUserDtoPasswordHashType as UpdateUserDtoPasswordHashType from .models import UpdateWebhookEndpointDtoEvents as UpdateWebhookEndpointDtoEvents from .models import UpdateWebhookEndpointDtoStatus as UpdateWebhookEndpointDtoStatus +from .models import ( + UpdateWebhookEndpointDtoStatusLiteral as UpdateWebhookEndpointDtoStatusLiteral, +) from .models import UserIdentitiesGetItemProvider as UserIdentitiesGetItemProvider +from .models import ( + UserIdentitiesGetItemProviderLiteral as UserIdentitiesGetItemProviderLiteral, +) from .models import UserInviteState as UserInviteState from .models import ( UserOrganizationMembershipBaseListDataStatus as UserOrganizationMembershipBaseListDataStatus, ) from .models import UserOrganizationMembershipStatus as UserOrganizationMembershipStatus from .models import UserSessionsAuthMethod as UserSessionsAuthMethod +from .models import UserSessionsAuthMethodLiteral as UserSessionsAuthMethodLiteral from .models import UserSessionsStatus as UserSessionsStatus +from .models import UserSessionsStatusLiteral as UserSessionsStatusLiteral from .models import WebhookEndpointJsonStatus as WebhookEndpointJsonStatus from .models import WidgetSessionTokenDtoScopes as WidgetSessionTokenDtoScopes +from .models import ( + WidgetSessionTokenDtoScopesLiteral as WidgetSessionTokenDtoScopesLiteral, +) diff --git a/src/workos/common/models/__init__.py b/src/workos/common/models/__init__.py index 761a821d..38e0a4c5 100644 --- a/src/workos/common/models/__init__.py +++ b/src/workos/common/models/__init__.py @@ -3,67 +3,123 @@ from .audit_log_configuration_log_stream_state import ( AuditLogConfigurationLogStreamState as AuditLogConfigurationLogStreamState, ) +from .audit_log_configuration_log_stream_state_literal import ( + AuditLogConfigurationLogStreamStateLiteral as AuditLogConfigurationLogStreamStateLiteral, +) from .audit_log_configuration_log_stream_type import ( AuditLogConfigurationLogStreamType as AuditLogConfigurationLogStreamType, ) +from .audit_log_configuration_log_stream_type_literal import ( + AuditLogConfigurationLogStreamTypeLiteral as AuditLogConfigurationLogStreamTypeLiteral, +) from .audit_log_configuration_state import ( AuditLogConfigurationState as AuditLogConfigurationState, ) +from .audit_log_configuration_state_literal import ( + AuditLogConfigurationStateLiteral as AuditLogConfigurationStateLiteral, +) from .audit_log_export_json_state import ( AuditLogExportJsonState as AuditLogExportJsonState, ) -from .audit_log_export_state import AuditLogExportState as AuditLogExportState -from .audit_log_stream_state import AuditLogStreamState as AuditLogStreamState -from .audit_log_trail_state import AuditLogTrailState as AuditLogTrailState -from .auth_method_type import AuthMethodType as AuthMethodType +from .audit_log_export_json_state_literal import ( + AuditLogExportJsonStateLiteral as AuditLogExportJsonStateLiteral, +) from .authenticate_response_authentication_method import ( AuthenticateResponseAuthenticationMethod as AuthenticateResponseAuthenticationMethod, ) +from .authenticate_response_authentication_method_literal import ( + AuthenticateResponseAuthenticationMethodLiteral as AuthenticateResponseAuthenticationMethodLiteral, +) from .authentication_factor_enrolled_type import ( AuthenticationFactorEnrolledType as AuthenticationFactorEnrolledType, ) +from .authentication_factor_enrolled_type_literal import ( + AuthenticationFactorEnrolledTypeLiteral as AuthenticationFactorEnrolledTypeLiteral, +) from .authentication_factor_type import ( AuthenticationFactorType as AuthenticationFactorType, ) from .authentication_factors_create_request_type import ( AuthenticationFactorsCreateRequestType as AuthenticationFactorsCreateRequestType, ) +from .authentication_factors_create_request_type_literal import ( + AuthenticationFactorsCreateRequestTypeLiteral as AuthenticationFactorsCreateRequestTypeLiteral, +) from .connected_account_state import ConnectedAccountState as ConnectedAccountState +from .connected_account_state_literal import ( + ConnectedAccountStateLiteral as ConnectedAccountStateLiteral, +) from .connection_state import ConnectionState as ConnectionState +from .connection_state_literal import ConnectionStateLiteral as ConnectionStateLiteral from .connection_status import ConnectionStatus as ConnectionStatus +from .connection_status_literal import ( + ConnectionStatusLiteral as ConnectionStatusLiteral, +) from .connection_type import ConnectionType as ConnectionType +from .connection_type_literal import ConnectionTypeLiteral as ConnectionTypeLiteral from .create_user_dto_password_hash_type import ( CreateUserDtoPasswordHashType as CreateUserDtoPasswordHashType, ) +from .create_user_dto_password_hash_type_literal import ( + CreateUserDtoPasswordHashTypeLiteral as CreateUserDtoPasswordHashTypeLiteral, +) from .create_user_invite_options_dto_locale import ( CreateUserInviteOptionsDtoLocale as CreateUserInviteOptionsDtoLocale, ) +from .create_user_invite_options_dto_locale_literal import ( + CreateUserInviteOptionsDtoLocaleLiteral as CreateUserInviteOptionsDtoLocaleLiteral, +) from .create_webhook_endpoint_dto_events import ( CreateWebhookEndpointDtoEvents as CreateWebhookEndpointDtoEvents, ) +from .create_webhook_endpoint_dto_events_literal import ( + CreateWebhookEndpointDtoEventsLiteral as CreateWebhookEndpointDtoEventsLiteral, +) from .data_integrations_list_response_data_connected_account_state import ( DataIntegrationsListResponseDataConnectedAccountState as DataIntegrationsListResponseDataConnectedAccountState, ) from .data_integrations_list_response_data_ownership import ( DataIntegrationsListResponseDataOwnership as DataIntegrationsListResponseDataOwnership, ) +from .data_integrations_list_response_data_ownership_literal import ( + DataIntegrationsListResponseDataOwnershipLiteral as DataIntegrationsListResponseDataOwnershipLiteral, +) from .directory_state import DirectoryState as DirectoryState +from .directory_state_literal import DirectoryStateLiteral as DirectoryStateLiteral from .directory_type import DirectoryType as DirectoryType +from .directory_type_literal import DirectoryTypeLiteral as DirectoryTypeLiteral from .directory_user_with_groups_state import ( DirectoryUserWithGroupsState as DirectoryUserWithGroupsState, ) +from .directory_user_with_groups_state_literal import ( + DirectoryUserWithGroupsStateLiteral as DirectoryUserWithGroupsStateLiteral, +) from .generate_link_dto_intent import GenerateLinkDtoIntent as GenerateLinkDtoIntent +from .generate_link_dto_intent_literal import ( + GenerateLinkDtoIntentLiteral as GenerateLinkDtoIntentLiteral, +) from .invitation_state import InvitationState as InvitationState +from .invitation_state_literal import InvitationStateLiteral as InvitationStateLiteral from .list_data_type import ListDataType as ListDataType +from .list_data_type_literal import ListDataTypeLiteral as ListDataTypeLiteral from .organization_domain_data_dto_state import ( OrganizationDomainDataDtoState as OrganizationDomainDataDtoState, ) +from .organization_domain_data_dto_state_literal import ( + OrganizationDomainDataDtoStateLiteral as OrganizationDomainDataDtoStateLiteral, +) from .organization_domain_stand_alone_state import ( OrganizationDomainStandAloneState as OrganizationDomainStandAloneState, ) +from .organization_domain_stand_alone_state_literal import ( + OrganizationDomainStandAloneStateLiteral as OrganizationDomainStandAloneStateLiteral, +) from .organization_domain_stand_alone_verification_strategy import ( OrganizationDomainStandAloneVerificationStrategy as OrganizationDomainStandAloneVerificationStrategy, ) +from .organization_domain_stand_alone_verification_strategy_literal import ( + OrganizationDomainStandAloneVerificationStrategyLiteral as OrganizationDomainStandAloneVerificationStrategyLiteral, +) from .organization_domain_state import ( OrganizationDomainState as OrganizationDomainState, ) @@ -73,23 +129,34 @@ from .organization_membership_status import ( OrganizationMembershipStatus as OrganizationMembershipStatus, ) -from .password_hash_type import PasswordHashType as PasswordHashType from .profile_connection_type import ProfileConnectionType as ProfileConnectionType from .radar_standalone_assess_request_action import ( RadarStandaloneAssessRequestAction as RadarStandaloneAssessRequestAction, ) +from .radar_standalone_assess_request_action_literal import ( + RadarStandaloneAssessRequestActionLiteral as RadarStandaloneAssessRequestActionLiteral, +) from .radar_standalone_assess_request_auth_method import ( RadarStandaloneAssessRequestAuthMethod as RadarStandaloneAssessRequestAuthMethod, ) +from .radar_standalone_assess_request_auth_method_literal import ( + RadarStandaloneAssessRequestAuthMethodLiteral as RadarStandaloneAssessRequestAuthMethodLiteral, +) from .radar_standalone_response_blocklist_type import ( RadarStandaloneResponseBlocklistType as RadarStandaloneResponseBlocklistType, ) from .radar_standalone_response_control import ( RadarStandaloneResponseControl as RadarStandaloneResponseControl, ) +from .radar_standalone_response_control_literal import ( + RadarStandaloneResponseControlLiteral as RadarStandaloneResponseControlLiteral, +) from .radar_standalone_response_verdict import ( RadarStandaloneResponseVerdict as RadarStandaloneResponseVerdict, ) +from .radar_standalone_response_verdict_literal import ( + RadarStandaloneResponseVerdictLiteral as RadarStandaloneResponseVerdictLiteral, +) from .resend_user_invite_options_dto_locale import ( ResendUserInviteOptionsDtoLocale as ResendUserInviteOptionsDtoLocale, ) @@ -103,9 +170,15 @@ from .update_webhook_endpoint_dto_status import ( UpdateWebhookEndpointDtoStatus as UpdateWebhookEndpointDtoStatus, ) +from .update_webhook_endpoint_dto_status_literal import ( + UpdateWebhookEndpointDtoStatusLiteral as UpdateWebhookEndpointDtoStatusLiteral, +) from .user_identities_get_item_provider import ( UserIdentitiesGetItemProvider as UserIdentitiesGetItemProvider, ) +from .user_identities_get_item_provider_literal import ( + UserIdentitiesGetItemProviderLiteral as UserIdentitiesGetItemProviderLiteral, +) from .user_invite_state import UserInviteState as UserInviteState from .user_organization_membership_base_list_data_status import ( UserOrganizationMembershipBaseListDataStatus as UserOrganizationMembershipBaseListDataStatus, @@ -114,10 +187,19 @@ UserOrganizationMembershipStatus as UserOrganizationMembershipStatus, ) from .user_sessions_auth_method import UserSessionsAuthMethod as UserSessionsAuthMethod +from .user_sessions_auth_method_literal import ( + UserSessionsAuthMethodLiteral as UserSessionsAuthMethodLiteral, +) from .user_sessions_status import UserSessionsStatus as UserSessionsStatus +from .user_sessions_status_literal import ( + UserSessionsStatusLiteral as UserSessionsStatusLiteral, +) from .webhook_endpoint_json_status import ( WebhookEndpointJsonStatus as WebhookEndpointJsonStatus, ) from .widget_session_token_dto_scopes import ( WidgetSessionTokenDtoScopes as WidgetSessionTokenDtoScopes, ) +from .widget_session_token_dto_scopes_literal import ( + WidgetSessionTokenDtoScopesLiteral as WidgetSessionTokenDtoScopesLiteral, +) diff --git a/src/workos/common/models/audit_log_configuration_log_stream_state_literal.py b/src/workos/common/models/audit_log_configuration_log_stream_state_literal.py new file mode 100644 index 00000000..180785c9 --- /dev/null +++ b/src/workos/common/models/audit_log_configuration_log_stream_state_literal.py @@ -0,0 +1,8 @@ +# This file is auto-generated by oagen. Do not edit. + +from .audit_log_configuration_log_stream_state import ( + AuditLogConfigurationLogStreamState, +) + +AuditLogConfigurationLogStreamStateLiteral = AuditLogConfigurationLogStreamState +__all__ = ["AuditLogConfigurationLogStreamStateLiteral"] diff --git a/src/workos/common/models/audit_log_configuration_log_stream_type_literal.py b/src/workos/common/models/audit_log_configuration_log_stream_type_literal.py new file mode 100644 index 00000000..40d388cd --- /dev/null +++ b/src/workos/common/models/audit_log_configuration_log_stream_type_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .audit_log_configuration_log_stream_type import AuditLogConfigurationLogStreamType + +AuditLogConfigurationLogStreamTypeLiteral = AuditLogConfigurationLogStreamType +__all__ = ["AuditLogConfigurationLogStreamTypeLiteral"] diff --git a/src/workos/common/models/audit_log_configuration_state_literal.py b/src/workos/common/models/audit_log_configuration_state_literal.py new file mode 100644 index 00000000..c67ed974 --- /dev/null +++ b/src/workos/common/models/audit_log_configuration_state_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .audit_log_configuration_state import AuditLogConfigurationState + +AuditLogConfigurationStateLiteral = AuditLogConfigurationState +__all__ = ["AuditLogConfigurationStateLiteral"] diff --git a/src/workos/common/models/audit_log_export_json_state_literal.py b/src/workos/common/models/audit_log_export_json_state_literal.py new file mode 100644 index 00000000..8a1910f9 --- /dev/null +++ b/src/workos/common/models/audit_log_export_json_state_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .audit_log_export_json_state import AuditLogExportJsonState + +AuditLogExportJsonStateLiteral = AuditLogExportJsonState +__all__ = ["AuditLogExportJsonStateLiteral"] diff --git a/src/workos/common/models/authenticate_response_authentication_method_literal.py b/src/workos/common/models/authenticate_response_authentication_method_literal.py new file mode 100644 index 00000000..f3499481 --- /dev/null +++ b/src/workos/common/models/authenticate_response_authentication_method_literal.py @@ -0,0 +1,10 @@ +# This file is auto-generated by oagen. Do not edit. + +from .authenticate_response_authentication_method import ( + AuthenticateResponseAuthenticationMethod, +) + +AuthenticateResponseAuthenticationMethodLiteral = ( + AuthenticateResponseAuthenticationMethod +) +__all__ = ["AuthenticateResponseAuthenticationMethodLiteral"] diff --git a/src/workos/common/models/authentication_factor_enrolled_type_literal.py b/src/workos/common/models/authentication_factor_enrolled_type_literal.py new file mode 100644 index 00000000..87bae6ea --- /dev/null +++ b/src/workos/common/models/authentication_factor_enrolled_type_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .authentication_factor_enrolled_type import AuthenticationFactorEnrolledType + +AuthenticationFactorEnrolledTypeLiteral = AuthenticationFactorEnrolledType +__all__ = ["AuthenticationFactorEnrolledTypeLiteral"] diff --git a/src/workos/common/models/authentication_factors_create_request_type_literal.py b/src/workos/common/models/authentication_factors_create_request_type_literal.py new file mode 100644 index 00000000..56c0f7c1 --- /dev/null +++ b/src/workos/common/models/authentication_factors_create_request_type_literal.py @@ -0,0 +1,8 @@ +# This file is auto-generated by oagen. Do not edit. + +from .authentication_factors_create_request_type import ( + AuthenticationFactorsCreateRequestType, +) + +AuthenticationFactorsCreateRequestTypeLiteral = AuthenticationFactorsCreateRequestType +__all__ = ["AuthenticationFactorsCreateRequestTypeLiteral"] diff --git a/src/workos/common/models/connected_account_state_literal.py b/src/workos/common/models/connected_account_state_literal.py new file mode 100644 index 00000000..bf14254b --- /dev/null +++ b/src/workos/common/models/connected_account_state_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .connected_account_state import ConnectedAccountState + +ConnectedAccountStateLiteral = ConnectedAccountState +__all__ = ["ConnectedAccountStateLiteral"] diff --git a/src/workos/common/models/connection_state_literal.py b/src/workos/common/models/connection_state_literal.py new file mode 100644 index 00000000..f84a14ea --- /dev/null +++ b/src/workos/common/models/connection_state_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .connection_state import ConnectionState + +ConnectionStateLiteral = ConnectionState +__all__ = ["ConnectionStateLiteral"] diff --git a/src/workos/common/models/connection_status_literal.py b/src/workos/common/models/connection_status_literal.py new file mode 100644 index 00000000..9eea2a49 --- /dev/null +++ b/src/workos/common/models/connection_status_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .connection_status import ConnectionStatus + +ConnectionStatusLiteral = ConnectionStatus +__all__ = ["ConnectionStatusLiteral"] diff --git a/src/workos/common/models/connection_type_literal.py b/src/workos/common/models/connection_type_literal.py new file mode 100644 index 00000000..91c72983 --- /dev/null +++ b/src/workos/common/models/connection_type_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .connection_type import ConnectionType + +ConnectionTypeLiteral = ConnectionType +__all__ = ["ConnectionTypeLiteral"] diff --git a/src/workos/common/models/create_user_dto_password_hash_type_literal.py b/src/workos/common/models/create_user_dto_password_hash_type_literal.py new file mode 100644 index 00000000..38b2704f --- /dev/null +++ b/src/workos/common/models/create_user_dto_password_hash_type_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .create_user_dto_password_hash_type import CreateUserDtoPasswordHashType + +CreateUserDtoPasswordHashTypeLiteral = CreateUserDtoPasswordHashType +__all__ = ["CreateUserDtoPasswordHashTypeLiteral"] diff --git a/src/workos/common/models/create_user_invite_options_dto_locale_literal.py b/src/workos/common/models/create_user_invite_options_dto_locale_literal.py new file mode 100644 index 00000000..1f582337 --- /dev/null +++ b/src/workos/common/models/create_user_invite_options_dto_locale_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .create_user_invite_options_dto_locale import CreateUserInviteOptionsDtoLocale + +CreateUserInviteOptionsDtoLocaleLiteral = CreateUserInviteOptionsDtoLocale +__all__ = ["CreateUserInviteOptionsDtoLocaleLiteral"] diff --git a/src/workos/common/models/create_webhook_endpoint_dto_events_literal.py b/src/workos/common/models/create_webhook_endpoint_dto_events_literal.py new file mode 100644 index 00000000..fde7c801 --- /dev/null +++ b/src/workos/common/models/create_webhook_endpoint_dto_events_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .create_webhook_endpoint_dto_events import CreateWebhookEndpointDtoEvents + +CreateWebhookEndpointDtoEventsLiteral = CreateWebhookEndpointDtoEvents +__all__ = ["CreateWebhookEndpointDtoEventsLiteral"] diff --git a/src/workos/common/models/data_integrations_list_response_data_ownership_literal.py b/src/workos/common/models/data_integrations_list_response_data_ownership_literal.py new file mode 100644 index 00000000..c20e01d2 --- /dev/null +++ b/src/workos/common/models/data_integrations_list_response_data_ownership_literal.py @@ -0,0 +1,10 @@ +# This file is auto-generated by oagen. Do not edit. + +from .data_integrations_list_response_data_ownership import ( + DataIntegrationsListResponseDataOwnership, +) + +DataIntegrationsListResponseDataOwnershipLiteral = ( + DataIntegrationsListResponseDataOwnership +) +__all__ = ["DataIntegrationsListResponseDataOwnershipLiteral"] diff --git a/src/workos/common/models/directory_state_literal.py b/src/workos/common/models/directory_state_literal.py new file mode 100644 index 00000000..166d9b24 --- /dev/null +++ b/src/workos/common/models/directory_state_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .directory_state import DirectoryState + +DirectoryStateLiteral = DirectoryState +__all__ = ["DirectoryStateLiteral"] diff --git a/src/workos/common/models/directory_type_literal.py b/src/workos/common/models/directory_type_literal.py new file mode 100644 index 00000000..54ea0f3c --- /dev/null +++ b/src/workos/common/models/directory_type_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .directory_type import DirectoryType + +DirectoryTypeLiteral = DirectoryType +__all__ = ["DirectoryTypeLiteral"] diff --git a/src/workos/common/models/directory_user_with_groups_state_literal.py b/src/workos/common/models/directory_user_with_groups_state_literal.py new file mode 100644 index 00000000..49d12c21 --- /dev/null +++ b/src/workos/common/models/directory_user_with_groups_state_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .directory_user_with_groups_state import DirectoryUserWithGroupsState + +DirectoryUserWithGroupsStateLiteral = DirectoryUserWithGroupsState +__all__ = ["DirectoryUserWithGroupsStateLiteral"] diff --git a/src/workos/common/models/generate_link_dto_intent_literal.py b/src/workos/common/models/generate_link_dto_intent_literal.py new file mode 100644 index 00000000..a5672e87 --- /dev/null +++ b/src/workos/common/models/generate_link_dto_intent_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .generate_link_dto_intent import GenerateLinkDtoIntent + +GenerateLinkDtoIntentLiteral = GenerateLinkDtoIntent +__all__ = ["GenerateLinkDtoIntentLiteral"] diff --git a/src/workos/common/models/invitation_state_literal.py b/src/workos/common/models/invitation_state_literal.py new file mode 100644 index 00000000..a9d15d0a --- /dev/null +++ b/src/workos/common/models/invitation_state_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .invitation_state import InvitationState + +InvitationStateLiteral = InvitationState +__all__ = ["InvitationStateLiteral"] diff --git a/src/workos/common/models/list_data_type_literal.py b/src/workos/common/models/list_data_type_literal.py new file mode 100644 index 00000000..2234cda5 --- /dev/null +++ b/src/workos/common/models/list_data_type_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .list_data_type import ListDataType + +ListDataTypeLiteral = ListDataType +__all__ = ["ListDataTypeLiteral"] diff --git a/src/workos/common/models/organization_domain_data_dto_state_literal.py b/src/workos/common/models/organization_domain_data_dto_state_literal.py new file mode 100644 index 00000000..820a0594 --- /dev/null +++ b/src/workos/common/models/organization_domain_data_dto_state_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .organization_domain_data_dto_state import OrganizationDomainDataDtoState + +OrganizationDomainDataDtoStateLiteral = OrganizationDomainDataDtoState +__all__ = ["OrganizationDomainDataDtoStateLiteral"] diff --git a/src/workos/common/models/organization_domain_stand_alone_state_literal.py b/src/workos/common/models/organization_domain_stand_alone_state_literal.py new file mode 100644 index 00000000..ef5ed8ae --- /dev/null +++ b/src/workos/common/models/organization_domain_stand_alone_state_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .organization_domain_stand_alone_state import OrganizationDomainStandAloneState + +OrganizationDomainStandAloneStateLiteral = OrganizationDomainStandAloneState +__all__ = ["OrganizationDomainStandAloneStateLiteral"] diff --git a/src/workos/common/models/organization_domain_stand_alone_verification_strategy_literal.py b/src/workos/common/models/organization_domain_stand_alone_verification_strategy_literal.py new file mode 100644 index 00000000..80ad2f79 --- /dev/null +++ b/src/workos/common/models/organization_domain_stand_alone_verification_strategy_literal.py @@ -0,0 +1,10 @@ +# This file is auto-generated by oagen. Do not edit. + +from .organization_domain_stand_alone_verification_strategy import ( + OrganizationDomainStandAloneVerificationStrategy, +) + +OrganizationDomainStandAloneVerificationStrategyLiteral = ( + OrganizationDomainStandAloneVerificationStrategy +) +__all__ = ["OrganizationDomainStandAloneVerificationStrategyLiteral"] diff --git a/src/workos/common/models/radar_standalone_assess_request_action_literal.py b/src/workos/common/models/radar_standalone_assess_request_action_literal.py new file mode 100644 index 00000000..ff029130 --- /dev/null +++ b/src/workos/common/models/radar_standalone_assess_request_action_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .radar_standalone_assess_request_action import RadarStandaloneAssessRequestAction + +RadarStandaloneAssessRequestActionLiteral = RadarStandaloneAssessRequestAction +__all__ = ["RadarStandaloneAssessRequestActionLiteral"] diff --git a/src/workos/common/models/radar_standalone_assess_request_auth_method_literal.py b/src/workos/common/models/radar_standalone_assess_request_auth_method_literal.py new file mode 100644 index 00000000..559b72a0 --- /dev/null +++ b/src/workos/common/models/radar_standalone_assess_request_auth_method_literal.py @@ -0,0 +1,8 @@ +# This file is auto-generated by oagen. Do not edit. + +from .radar_standalone_assess_request_auth_method import ( + RadarStandaloneAssessRequestAuthMethod, +) + +RadarStandaloneAssessRequestAuthMethodLiteral = RadarStandaloneAssessRequestAuthMethod +__all__ = ["RadarStandaloneAssessRequestAuthMethodLiteral"] diff --git a/src/workos/common/models/radar_standalone_response_control_literal.py b/src/workos/common/models/radar_standalone_response_control_literal.py new file mode 100644 index 00000000..5913ef98 --- /dev/null +++ b/src/workos/common/models/radar_standalone_response_control_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .radar_standalone_response_control import RadarStandaloneResponseControl + +RadarStandaloneResponseControlLiteral = RadarStandaloneResponseControl +__all__ = ["RadarStandaloneResponseControlLiteral"] diff --git a/src/workos/common/models/radar_standalone_response_verdict_literal.py b/src/workos/common/models/radar_standalone_response_verdict_literal.py new file mode 100644 index 00000000..e0bb77c7 --- /dev/null +++ b/src/workos/common/models/radar_standalone_response_verdict_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .radar_standalone_response_verdict import RadarStandaloneResponseVerdict + +RadarStandaloneResponseVerdictLiteral = RadarStandaloneResponseVerdict +__all__ = ["RadarStandaloneResponseVerdictLiteral"] diff --git a/src/workos/common/models/update_webhook_endpoint_dto_status_literal.py b/src/workos/common/models/update_webhook_endpoint_dto_status_literal.py new file mode 100644 index 00000000..b82552c0 --- /dev/null +++ b/src/workos/common/models/update_webhook_endpoint_dto_status_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .update_webhook_endpoint_dto_status import UpdateWebhookEndpointDtoStatus + +UpdateWebhookEndpointDtoStatusLiteral = UpdateWebhookEndpointDtoStatus +__all__ = ["UpdateWebhookEndpointDtoStatusLiteral"] diff --git a/src/workos/common/models/user_identities_get_item_provider_literal.py b/src/workos/common/models/user_identities_get_item_provider_literal.py new file mode 100644 index 00000000..0ede73af --- /dev/null +++ b/src/workos/common/models/user_identities_get_item_provider_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .user_identities_get_item_provider import UserIdentitiesGetItemProvider + +UserIdentitiesGetItemProviderLiteral = UserIdentitiesGetItemProvider +__all__ = ["UserIdentitiesGetItemProviderLiteral"] diff --git a/src/workos/common/models/user_sessions_auth_method_literal.py b/src/workos/common/models/user_sessions_auth_method_literal.py new file mode 100644 index 00000000..7c131e97 --- /dev/null +++ b/src/workos/common/models/user_sessions_auth_method_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .user_sessions_auth_method import UserSessionsAuthMethod + +UserSessionsAuthMethodLiteral = UserSessionsAuthMethod +__all__ = ["UserSessionsAuthMethodLiteral"] diff --git a/src/workos/common/models/user_sessions_status_literal.py b/src/workos/common/models/user_sessions_status_literal.py new file mode 100644 index 00000000..dd739263 --- /dev/null +++ b/src/workos/common/models/user_sessions_status_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .user_sessions_status import UserSessionsStatus + +UserSessionsStatusLiteral = UserSessionsStatus +__all__ = ["UserSessionsStatusLiteral"] diff --git a/src/workos/common/models/widget_session_token_dto_scopes_literal.py b/src/workos/common/models/widget_session_token_dto_scopes_literal.py new file mode 100644 index 00000000..5e02313d --- /dev/null +++ b/src/workos/common/models/widget_session_token_dto_scopes_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .widget_session_token_dto_scopes import WidgetSessionTokenDtoScopes + +WidgetSessionTokenDtoScopesLiteral = WidgetSessionTokenDtoScopes +__all__ = ["WidgetSessionTokenDtoScopesLiteral"] diff --git a/src/workos/connections/_resource.py b/src/workos/connections/_resource.py index ca6405b0..fab2f8a9 100644 --- a/src/workos/connections/_resource.py +++ b/src/workos/connections/_resource.py @@ -2,15 +2,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Union if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient +from .._types import RequestOptions, enum_value from .models import Connection from .models import ConnectionsConnectionType, ConnectionsOrder from .._pagination import AsyncPage, SyncPage -from .._types import RequestOptions class Connections: @@ -25,8 +25,8 @@ def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[ConnectionsOrder] = None, - connection_type: Optional[ConnectionsConnectionType] = None, + order: Optional[Union[ConnectionsOrder, str]] = None, + connection_type: Optional[Union[ConnectionsConnectionType, str]] = None, domain: Optional[str] = None, organization_id: Optional[str] = None, search: Optional[str] = None, @@ -63,8 +63,10 @@ def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, - "connection_type": connection_type.value if connection_type else None, + "order": enum_value(order) if order is not None else None, + "connection_type": enum_value(connection_type) + if connection_type is not None + else None, "domain": domain, "organization_id": organization_id, "search": search, @@ -150,8 +152,8 @@ async def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[ConnectionsOrder] = None, - connection_type: Optional[ConnectionsConnectionType] = None, + order: Optional[Union[ConnectionsOrder, str]] = None, + connection_type: Optional[Union[ConnectionsConnectionType, str]] = None, domain: Optional[str] = None, organization_id: Optional[str] = None, search: Optional[str] = None, @@ -188,8 +190,10 @@ async def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, - "connection_type": connection_type.value if connection_type else None, + "order": enum_value(order) if order is not None else None, + "connection_type": enum_value(connection_type) + if connection_type is not None + else None, "domain": domain, "organization_id": organization_id, "search": search, diff --git a/src/workos/connections/models/__init__.py b/src/workos/connections/models/__init__.py index e19062df..7c61aef5 100644 --- a/src/workos/connections/models/__init__.py +++ b/src/workos/connections/models/__init__.py @@ -6,4 +6,7 @@ from .connections_connection_type import ( ConnectionsConnectionType as ConnectionsConnectionType, ) +from .connections_connection_type_literal import ( + ConnectionsConnectionTypeLiteral as ConnectionsConnectionTypeLiteral, +) from .connections_order import ConnectionsOrder as ConnectionsOrder diff --git a/src/workos/connections/models/connections_connection_type_literal.py b/src/workos/connections/models/connections_connection_type_literal.py new file mode 100644 index 00000000..8a987957 --- /dev/null +++ b/src/workos/connections/models/connections_connection_type_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .connections_connection_type import ConnectionsConnectionType + +ConnectionsConnectionTypeLiteral = ConnectionsConnectionType +__all__ = ["ConnectionsConnectionTypeLiteral"] diff --git a/src/workos/directories/_resource.py b/src/workos/directories/_resource.py index 764e05ab..c6413bb6 100644 --- a/src/workos/directories/_resource.py +++ b/src/workos/directories/_resource.py @@ -2,15 +2,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Union if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient +from .._types import RequestOptions, enum_value from .models import Directory from .models import DirectoriesOrder from .._pagination import AsyncPage, SyncPage -from .._types import RequestOptions class Directories: @@ -25,7 +25,7 @@ def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[DirectoriesOrder] = None, + order: Optional[Union[DirectoriesOrder, str]] = None, organization_id: Optional[str] = None, search: Optional[str] = None, domain: Optional[str] = None, @@ -61,7 +61,7 @@ def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "organization_id": organization_id, "search": search, "domain": domain, @@ -146,7 +146,7 @@ async def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[DirectoriesOrder] = None, + order: Optional[Union[DirectoriesOrder, str]] = None, organization_id: Optional[str] = None, search: Optional[str] = None, domain: Optional[str] = None, @@ -182,7 +182,7 @@ async def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "organization_id": organization_id, "search": search, "domain": domain, diff --git a/src/workos/directory_groups/_resource.py b/src/workos/directory_groups/_resource.py index ec891fd8..e2f92820 100644 --- a/src/workos/directory_groups/_resource.py +++ b/src/workos/directory_groups/_resource.py @@ -2,15 +2,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Union if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient +from .._types import RequestOptions, enum_value from .models import DirectoryGroup from .models import DirectoryGroupsOrder from .._pagination import AsyncPage, SyncPage -from .._types import RequestOptions class DirectoryGroups: @@ -25,7 +25,7 @@ def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[DirectoryGroupsOrder] = None, + order: Optional[Union[DirectoryGroupsOrder, str]] = None, directory: Optional[str] = None, user: Optional[str] = None, request_options: Optional[RequestOptions] = None, @@ -60,7 +60,7 @@ def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "directory": directory, "user": user, }.items() @@ -118,7 +118,7 @@ async def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[DirectoryGroupsOrder] = None, + order: Optional[Union[DirectoryGroupsOrder, str]] = None, directory: Optional[str] = None, user: Optional[str] = None, request_options: Optional[RequestOptions] = None, @@ -153,7 +153,7 @@ async def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "directory": directory, "user": user, }.items() diff --git a/src/workos/directory_users/_resource.py b/src/workos/directory_users/_resource.py index 1efc7da5..28fe5126 100644 --- a/src/workos/directory_users/_resource.py +++ b/src/workos/directory_users/_resource.py @@ -2,15 +2,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Union if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient +from .._types import RequestOptions, enum_value from .models import DirectoryUserWithGroups from .models import DirectoryUsersOrder from .._pagination import AsyncPage, SyncPage -from .._types import RequestOptions class DirectoryUsers: @@ -25,7 +25,7 @@ def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[DirectoryUsersOrder] = None, + order: Optional[Union[DirectoryUsersOrder, str]] = None, directory: Optional[str] = None, group: Optional[str] = None, request_options: Optional[RequestOptions] = None, @@ -60,7 +60,7 @@ def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "directory": directory, "group": group, }.items() @@ -118,7 +118,7 @@ async def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[DirectoryUsersOrder] = None, + order: Optional[Union[DirectoryUsersOrder, str]] = None, directory: Optional[str] = None, group: Optional[str] = None, request_options: Optional[RequestOptions] = None, @@ -153,7 +153,7 @@ async def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "directory": directory, "group": group, }.items() diff --git a/src/workos/events/_resource.py b/src/workos/events/_resource.py index 04384e15..077d7bd7 100644 --- a/src/workos/events/_resource.py +++ b/src/workos/events/_resource.py @@ -2,15 +2,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, List, Optional, Union if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient +from .._types import RequestOptions, enum_value from .models import Event from .models import EventsOrder from .._pagination import AsyncPage, SyncPage -from .._types import RequestOptions class Events: @@ -19,13 +19,13 @@ class Events: def __init__(self, client: "WorkOSClient") -> None: self._client = client - def list_events( + def list( self, *, limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[EventsOrder] = None, + order: Optional[Union[EventsOrder, str]] = None, events: Optional[List[str]] = None, range_start: Optional[str] = None, range_end: Optional[str] = None, @@ -63,7 +63,7 @@ def list_events( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "events": events, "range_start": range_start, "range_end": range_end, @@ -86,13 +86,13 @@ class AsyncEvents: def __init__(self, client: "AsyncWorkOSClient") -> None: self._client = client - async def list_events( + async def list( self, *, limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[EventsOrder] = None, + order: Optional[Union[EventsOrder, str]] = None, events: Optional[List[str]] = None, range_start: Optional[str] = None, range_end: Optional[str] = None, @@ -130,7 +130,7 @@ async def list_events( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "events": events, "range_start": range_start, "range_end": range_end, diff --git a/src/workos/events/models/event.py b/src/workos/events/models/event.py index d0640e4e..bb5accd2 100644 --- a/src/workos/events/models/event.py +++ b/src/workos/events/models/event.py @@ -3,42 +3,21 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime -from typing import Any, Dict, Literal, Optional +from typing import Any, Dict from workos._errors import BaseRequestException @dataclass(slots=True) class Event: - """Event model.""" + """An event emitted by WorkOS.""" - object: Literal["event"] - """Distinguishes the Event object.""" - id: str - """Unique identifier for the Event.""" - event: str - """The type of event that occurred.""" - data: Dict[str, Any] - """The event payload.""" - created_at: datetime - """An ISO 8601 timestamp.""" - context: Optional[Dict[str, Any]] = None - """Additional context about the event.""" + pass @classmethod def from_dict(cls, data: Dict[str, Any]) -> "Event": """Deserialize from a dictionary.""" try: - return cls( - object=data["object"], - id=data["id"], - event=data["event"], - data=data["data"], - created_at=datetime.fromisoformat( - data["created_at"].replace("Z", "+00:00") - ), - context=data.get("context"), - ) + return cls() except (KeyError, ValueError) as e: raise BaseRequestException( f"Unexpected API response while parsing Event: {e!s}" @@ -47,13 +26,4 @@ def from_dict(cls, data: Dict[str, Any]) -> "Event": def to_dict(self) -> Dict[str, Any]: """Serialize to a dictionary.""" result: Dict[str, Any] = {} - result["object"] = self.object - result["id"] = self.id - result["event"] = self.event - result["data"] = self.data - result["created_at"] = self.created_at.isoformat( - timespec="milliseconds" - ).replace("+00:00", "Z") - if self.context is not None: - result["context"] = self.context return result diff --git a/src/workos/feature_flags/_resource.py b/src/workos/feature_flags/_resource.py index 8c00d9b9..a3f3e12b 100644 --- a/src/workos/feature_flags/_resource.py +++ b/src/workos/feature_flags/_resource.py @@ -2,15 +2,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Union if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient +from .._types import RequestOptions, enum_value from .models import FeatureFlag, Flag from .models import FeatureFlagsOrder from .._pagination import AsyncPage, SyncPage -from .._types import RequestOptions class FeatureFlags: @@ -25,7 +25,7 @@ def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[FeatureFlagsOrder] = None, + order: Optional[Union[FeatureFlagsOrder, str]] = None, request_options: Optional[RequestOptions] = None, ) -> SyncPage[Flag]: """List feature flags @@ -56,7 +56,7 @@ def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } @@ -171,7 +171,7 @@ async def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[FeatureFlagsOrder] = None, + order: Optional[Union[FeatureFlagsOrder, str]] = None, request_options: Optional[RequestOptions] = None, ) -> AsyncPage[Flag]: """List feature flags @@ -202,7 +202,7 @@ async def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } diff --git a/src/workos/multi_factor_auth/_resource.py b/src/workos/multi_factor_auth/_resource.py index 3d27d8c9..1b5428ec 100644 --- a/src/workos/multi_factor_auth/_resource.py +++ b/src/workos/multi_factor_auth/_resource.py @@ -2,15 +2,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import TYPE_CHECKING, Any, Dict, Optional, Union if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient +from .._types import RequestOptions, enum_value from .models import AuthenticationFactor, AuthenticationFactorEnrolled from workos.multi_factor_auth.challenges.models import AuthenticationChallenge from workos.common.models import AuthenticationFactorsCreateRequestType -from .._types import RequestOptions class MultiFactorAuth: @@ -22,7 +22,7 @@ def __init__(self, client: "WorkOSClient") -> None: def create( self, *, - type: AuthenticationFactorsCreateRequestType, + type: Union[AuthenticationFactorsCreateRequestType, str], phone_number: Optional[str] = None, totp_issuer: Optional[str] = None, totp_user: Optional[str] = None, @@ -53,7 +53,7 @@ def create( body: Dict[str, Any] = { k: v for k, v in { - "type": type, + "type": enum_value(type), "phone_number": phone_number, "totp_issuer": totp_issuer, "totp_user": totp_user, @@ -176,7 +176,7 @@ def __init__(self, client: "AsyncWorkOSClient") -> None: async def create( self, *, - type: AuthenticationFactorsCreateRequestType, + type: Union[AuthenticationFactorsCreateRequestType, str], phone_number: Optional[str] = None, totp_issuer: Optional[str] = None, totp_user: Optional[str] = None, @@ -207,7 +207,7 @@ async def create( body: Dict[str, Any] = { k: v for k, v in { - "type": type, + "type": enum_value(type), "phone_number": phone_number, "totp_issuer": totp_issuer, "totp_user": totp_user, diff --git a/src/workos/multi_factor_auth/challenges/_resource.py b/src/workos/multi_factor_auth/challenges/_resource.py index 06d149ae..50db6b6a 100644 --- a/src/workos/multi_factor_auth/challenges/_resource.py +++ b/src/workos/multi_factor_auth/challenges/_resource.py @@ -7,8 +7,8 @@ if TYPE_CHECKING: from ..._client import AsyncWorkOSClient, WorkOSClient -from .models import AuthenticationChallengeVerifyResponse from ..._types import RequestOptions +from .models import AuthenticationChallengeVerifyResponse class MultiFactorAuthChallenges: diff --git a/src/workos/organization_domains/_resource.py b/src/workos/organization_domains/_resource.py index 699dfc7b..e037cdcc 100644 --- a/src/workos/organization_domains/_resource.py +++ b/src/workos/organization_domains/_resource.py @@ -7,8 +7,8 @@ if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient -from .models import OrganizationDomain, OrganizationDomainStandAlone from .._types import RequestOptions +from .models import OrganizationDomain, OrganizationDomainStandAlone class OrganizationDomains: @@ -110,7 +110,7 @@ def delete( request_options=request_options, ) - def verify_organization_domain( + def verify( self, id: str, *, @@ -240,7 +240,7 @@ async def delete( request_options=request_options, ) - async def verify_organization_domain( + async def verify( self, id: str, *, diff --git a/src/workos/organizations/_resource.py b/src/workos/organizations/_resource.py index 63fcfede..f92a189d 100644 --- a/src/workos/organizations/_resource.py +++ b/src/workos/organizations/_resource.py @@ -2,11 +2,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient +from .._types import RequestOptions, enum_value from .models import ( AuditLogConfiguration, AuditLogsRetentionJson, @@ -15,7 +16,6 @@ ) from .models import OrganizationsOrder from .._pagination import AsyncPage, SyncPage -from .._types import RequestOptions class Organizations: @@ -30,7 +30,7 @@ def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[OrganizationsOrder] = None, + order: Optional[Union[OrganizationsOrder, str]] = None, domains: Optional[List[str]] = None, search: Optional[str] = None, request_options: Optional[RequestOptions] = None, @@ -63,7 +63,7 @@ def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "domains": domains, "search": search, }.items() @@ -394,7 +394,7 @@ async def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[OrganizationsOrder] = None, + order: Optional[Union[OrganizationsOrder, str]] = None, domains: Optional[List[str]] = None, search: Optional[str] = None, request_options: Optional[RequestOptions] = None, @@ -427,7 +427,7 @@ async def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "domains": domains, "search": search, }.items() diff --git a/src/workos/organizations/api_keys/_resource.py b/src/workos/organizations/api_keys/_resource.py index 59eb2da2..ea799f59 100644 --- a/src/workos/organizations/api_keys/_resource.py +++ b/src/workos/organizations/api_keys/_resource.py @@ -2,16 +2,16 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union if TYPE_CHECKING: from ..._client import AsyncWorkOSClient, WorkOSClient +from ..._types import RequestOptions, enum_value from .models import ApiKeyWithValue from workos.api_keys.models import ApiKey from .models import OrganizationsApiKeysOrder from ..._pagination import AsyncPage, SyncPage -from ..._types import RequestOptions class OrganizationsApiKeys: @@ -27,7 +27,7 @@ def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[OrganizationsApiKeysOrder] = None, + order: Optional[Union[OrganizationsApiKeysOrder, str]] = None, request_options: Optional[RequestOptions] = None, ) -> SyncPage[ApiKey]: """List API keys for an organization @@ -57,7 +57,7 @@ def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } @@ -127,7 +127,7 @@ async def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[OrganizationsApiKeysOrder] = None, + order: Optional[Union[OrganizationsApiKeysOrder, str]] = None, request_options: Optional[RequestOptions] = None, ) -> AsyncPage[ApiKey]: """List API keys for an organization @@ -157,7 +157,7 @@ async def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } diff --git a/src/workos/organizations/feature_flags/_resource.py b/src/workos/organizations/feature_flags/_resource.py index 343c1a53..0d57f52a 100644 --- a/src/workos/organizations/feature_flags/_resource.py +++ b/src/workos/organizations/feature_flags/_resource.py @@ -2,15 +2,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Union if TYPE_CHECKING: from ..._client import AsyncWorkOSClient, WorkOSClient +from ..._types import RequestOptions, enum_value from workos.feature_flags.models import Flag from .models import OrganizationsFeatureFlagsOrder from ..._pagination import AsyncPage, SyncPage -from ..._types import RequestOptions class OrganizationsFeatureFlags: @@ -26,7 +26,7 @@ def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[OrganizationsFeatureFlagsOrder] = None, + order: Optional[Union[OrganizationsFeatureFlagsOrder, str]] = None, request_options: Optional[RequestOptions] = None, ) -> SyncPage[Flag]: """List enabled feature flags for an organization @@ -56,7 +56,7 @@ def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } @@ -82,7 +82,7 @@ async def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[OrganizationsFeatureFlagsOrder] = None, + order: Optional[Union[OrganizationsFeatureFlagsOrder, str]] = None, request_options: Optional[RequestOptions] = None, ) -> AsyncPage[Flag]: """List enabled feature flags for an organization @@ -112,7 +112,7 @@ async def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } diff --git a/src/workos/organizations/models/organization.py b/src/workos/organizations/models/organization.py index be8f0493..d97b08d5 100644 --- a/src/workos/organizations/models/organization.py +++ b/src/workos/organizations/models/organization.py @@ -31,10 +31,10 @@ class Organization: """An ISO 8601 timestamp.""" updated_at: datetime """An ISO 8601 timestamp.""" - allow_profiles_outside_organization: bool - """Whether the Organization allows profiles outside of its managed domains.""" stripe_customer_id: Optional[str] = None """The Stripe customer ID of the Organization.""" + allow_profiles_outside_organization: Optional[bool] = None + """Whether the Organization allows profiles outside of its managed domains.""" @classmethod def from_dict(cls, data: Dict[str, Any]) -> "Organization": @@ -56,10 +56,10 @@ def from_dict(cls, data: Dict[str, Any]) -> "Organization": updated_at=datetime.fromisoformat( data["updated_at"].replace("Z", "+00:00") ), - allow_profiles_outside_organization=data[ - "allow_profiles_outside_organization" - ], stripe_customer_id=data.get("stripe_customer_id"), + allow_profiles_outside_organization=data.get( + "allow_profiles_outside_organization" + ), ) except (KeyError, ValueError) as e: raise BaseRequestException( @@ -84,9 +84,10 @@ def to_dict(self) -> Dict[str, Any]: result["updated_at"] = self.updated_at.isoformat( timespec="milliseconds" ).replace("+00:00", "Z") - result["allow_profiles_outside_organization"] = ( - self.allow_profiles_outside_organization - ) if self.stripe_customer_id is not None: result["stripe_customer_id"] = self.stripe_customer_id + if self.allow_profiles_outside_organization is not None: + result["allow_profiles_outside_organization"] = ( + self.allow_profiles_outside_organization + ) return result diff --git a/src/workos/permissions/_resource.py b/src/workos/permissions/_resource.py index dca1a0cf..d0de1b17 100644 --- a/src/workos/permissions/_resource.py +++ b/src/workos/permissions/_resource.py @@ -2,15 +2,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import TYPE_CHECKING, Any, Dict, Optional, Union if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient +from .._types import RequestOptions, enum_value from .models import AuthorizationPermission, Permission from .models import PermissionsOrder from .._pagination import AsyncPage, SyncPage -from .._types import RequestOptions class Permissions: @@ -25,7 +25,7 @@ def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[PermissionsOrder] = None, + order: Optional[Union[PermissionsOrder, str]] = None, request_options: Optional[RequestOptions] = None, ) -> SyncPage[AuthorizationPermission]: """List permissions @@ -54,7 +54,7 @@ def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } @@ -231,7 +231,7 @@ async def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[PermissionsOrder] = None, + order: Optional[Union[PermissionsOrder, str]] = None, request_options: Optional[RequestOptions] = None, ) -> AsyncPage[AuthorizationPermission]: """List permissions @@ -260,7 +260,7 @@ async def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } diff --git a/src/workos/pipes/_resource.py b/src/workos/pipes/_resource.py index e574c004..21083505 100644 --- a/src/workos/pipes/_resource.py +++ b/src/workos/pipes/_resource.py @@ -7,11 +7,11 @@ if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient +from .._types import RequestOptions from .models import ( DataIntegrationAccessTokenResponse, DataIntegrationAuthorizeUrlResponse, ) -from .._types import RequestOptions class Pipes: diff --git a/src/workos/radar/_resource.py b/src/workos/radar/_resource.py index e60c29f9..64914a06 100644 --- a/src/workos/radar/_resource.py +++ b/src/workos/radar/_resource.py @@ -2,18 +2,18 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, Literal, Optional +from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Union if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient +from .._types import RequestOptions, enum_value from .models import RadarListEntryAlreadyPresentResponse, RadarStandaloneResponse from .models import RadarAction, RadarType from workos.common.models import ( RadarStandaloneAssessRequestAction, RadarStandaloneAssessRequestAuthMethod, ) -from .._types import RequestOptions class Radar: @@ -28,8 +28,8 @@ def assess( ip_address: str, user_agent: str, email: str, - auth_method: RadarStandaloneAssessRequestAuthMethod, - action: RadarStandaloneAssessRequestAction, + auth_method: Union[RadarStandaloneAssessRequestAuthMethod, str], + action: Union[RadarStandaloneAssessRequestAction, str], device_fingerprint: Optional[str] = None, bot_score: Optional[str] = None, request_options: Optional[RequestOptions] = None, @@ -63,8 +63,8 @@ def assess( "ip_address": ip_address, "user_agent": user_agent, "email": email, - "auth_method": auth_method, - "action": action, + "auth_method": enum_value(auth_method), + "action": enum_value(action), "device_fingerprint": device_fingerprint, "bot_score": bot_score, }.items() @@ -120,8 +120,8 @@ def update_radar_attempt( def update_radar_list( self, - type: RadarType, - action: RadarAction, + type: Union[RadarType, str], + action: Union[RadarAction, str], *, entry: str, request_options: Optional[RequestOptions] = None, @@ -158,8 +158,8 @@ def update_radar_list( def delete_radar_list_entry( self, - type: RadarType, - action: RadarAction, + type: Union[RadarType, str], + action: Union[RadarAction, str], *, entry: str, request_options: Optional[RequestOptions] = None, @@ -204,8 +204,8 @@ async def assess( ip_address: str, user_agent: str, email: str, - auth_method: RadarStandaloneAssessRequestAuthMethod, - action: RadarStandaloneAssessRequestAction, + auth_method: Union[RadarStandaloneAssessRequestAuthMethod, str], + action: Union[RadarStandaloneAssessRequestAction, str], device_fingerprint: Optional[str] = None, bot_score: Optional[str] = None, request_options: Optional[RequestOptions] = None, @@ -239,8 +239,8 @@ async def assess( "ip_address": ip_address, "user_agent": user_agent, "email": email, - "auth_method": auth_method, - "action": action, + "auth_method": enum_value(auth_method), + "action": enum_value(action), "device_fingerprint": device_fingerprint, "bot_score": bot_score, }.items() @@ -296,8 +296,8 @@ async def update_radar_attempt( async def update_radar_list( self, - type: RadarType, - action: RadarAction, + type: Union[RadarType, str], + action: Union[RadarAction, str], *, entry: str, request_options: Optional[RequestOptions] = None, @@ -334,8 +334,8 @@ async def update_radar_list( async def delete_radar_list_entry( self, - type: RadarType, - action: RadarAction, + type: Union[RadarType, str], + action: Union[RadarAction, str], *, entry: str, request_options: Optional[RequestOptions] = None, diff --git a/src/workos/radar/models/__init__.py b/src/workos/radar/models/__init__.py index afcc8cec..234c113c 100644 --- a/src/workos/radar/models/__init__.py +++ b/src/workos/radar/models/__init__.py @@ -1,6 +1,7 @@ # This file is auto-generated by oagen. Do not edit. from .radar_action import RadarAction as RadarAction +from .radar_action_literal import RadarActionLiteral as RadarActionLiteral from .radar_list_entry_already_present_response import ( RadarListEntryAlreadyPresentResponse as RadarListEntryAlreadyPresentResponse, ) @@ -13,6 +14,9 @@ from .radar_standalone_response import ( RadarStandaloneResponse as RadarStandaloneResponse, ) +from .radar_standalone_response_blocklist_type_literal import ( + RadarStandaloneResponseBlocklistTypeLiteral as RadarStandaloneResponseBlocklistTypeLiteral, +) from .radar_standalone_update_radar_attempt_request import ( RadarStandaloneUpdateRadarAttemptRequest as RadarStandaloneUpdateRadarAttemptRequest, ) diff --git a/src/workos/radar/models/radar_action_literal.py b/src/workos/radar/models/radar_action_literal.py new file mode 100644 index 00000000..a64b31d9 --- /dev/null +++ b/src/workos/radar/models/radar_action_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .radar_action import RadarAction + +RadarActionLiteral = RadarAction +__all__ = ["RadarActionLiteral"] diff --git a/src/workos/radar/models/radar_standalone_response_blocklist_type_literal.py b/src/workos/radar/models/radar_standalone_response_blocklist_type_literal.py new file mode 100644 index 00000000..fdb247a5 --- /dev/null +++ b/src/workos/radar/models/radar_standalone_response_blocklist_type_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.common.models import RadarStandaloneResponseBlocklistType + +RadarStandaloneResponseBlocklistTypeLiteral = RadarStandaloneResponseBlocklistType +__all__ = ["RadarStandaloneResponseBlocklistTypeLiteral"] diff --git a/src/workos/sso/_resource.py b/src/workos/sso/_resource.py index ff1d34ef..3cdf83ec 100644 --- a/src/workos/sso/_resource.py +++ b/src/workos/sso/_resource.py @@ -2,14 +2,14 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient +from .._types import RequestOptions, enum_value from .models import Profile, SSOLogoutAuthorizeResponse, SSOTokenResponse from .models import SSOProvider -from .._types import RequestOptions from ..connections.models import Connection, ConnectionsConnectionType, ConnectionsOrder from .._pagination import AsyncPage, SyncPage @@ -25,9 +25,9 @@ def authorize( *, provider_scopes: Optional[List[str]] = None, provider_query_params: Optional[Dict[str, str]] = None, - client_id: str, + client_id: Optional[str] = None, domain: Optional[str] = None, - provider: Optional[SSOProvider] = None, + provider: Optional[Union[SSOProvider, str]] = None, redirect_uri: str, response_type: Literal["code"], state: Optional[str] = None, @@ -79,7 +79,7 @@ def authorize( "provider_query_params": provider_query_params, "client_id": client_id, "domain": domain, - "provider": provider.value if provider else None, + "provider": enum_value(provider) if provider is not None else None, "redirect_uri": redirect_uri, "response_type": response_type, "state": state, @@ -91,6 +91,7 @@ def authorize( }.items() if v is not None } + params["client_id"] = params.get("client_id") or self._client.client_id return self._client.build_url("sso/authorize", params) def logout( @@ -200,11 +201,11 @@ def get_profile( request_options=request_options, ) - def get_profile_and_token( + def token( self, *, - client_id: str, - client_secret: str, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, code: str, grant_type: Literal["authorization_code"], request_options: Optional[RequestOptions] = None, @@ -237,6 +238,8 @@ def get_profile_and_token( "code": code, "grant_type": grant_type, } + body["client_id"] = body.get("client_id") or self._client.client_id + body["client_secret"] = body.get("client_secret") or self._client._api_key return self._client.request( method="post", path="sso/token", @@ -338,9 +341,9 @@ async def authorize( *, provider_scopes: Optional[List[str]] = None, provider_query_params: Optional[Dict[str, str]] = None, - client_id: str, + client_id: Optional[str] = None, domain: Optional[str] = None, - provider: Optional[SSOProvider] = None, + provider: Optional[Union[SSOProvider, str]] = None, redirect_uri: str, response_type: Literal["code"], state: Optional[str] = None, @@ -392,7 +395,7 @@ async def authorize( "provider_query_params": provider_query_params, "client_id": client_id, "domain": domain, - "provider": provider.value if provider else None, + "provider": enum_value(provider) if provider is not None else None, "redirect_uri": redirect_uri, "response_type": response_type, "state": state, @@ -404,6 +407,7 @@ async def authorize( }.items() if v is not None } + params["client_id"] = params.get("client_id") or self._client.client_id return self._client.build_url("sso/authorize", params) async def logout( @@ -513,11 +517,11 @@ async def get_profile( request_options=request_options, ) - async def get_profile_and_token( + async def token( self, *, - client_id: str, - client_secret: str, + client_id: Optional[str] = None, + client_secret: Optional[str] = None, code: str, grant_type: Literal["authorization_code"], request_options: Optional[RequestOptions] = None, @@ -550,6 +554,8 @@ async def get_profile_and_token( "code": code, "grant_type": grant_type, } + body["client_id"] = body.get("client_id") or self._client.client_id + body["client_secret"] = body.get("client_secret") or self._client._api_key return await self._client.request( method="post", path="sso/token", diff --git a/src/workos/sso/models/__init__.py b/src/workos/sso/models/__init__.py index a5b8c15d..b7cb04c3 100644 --- a/src/workos/sso/models/__init__.py +++ b/src/workos/sso/models/__init__.py @@ -11,6 +11,7 @@ SSOLogoutAuthorizeResponse as SSOLogoutAuthorizeResponse, ) from .sso_provider import SSOProvider as SSOProvider +from .sso_provider_literal import SSOProviderLiteral as SSOProviderLiteral from .sso_token_response import SSOTokenResponse as SSOTokenResponse from .sso_token_response_oauth_token import ( SSOTokenResponseOAuthToken as SSOTokenResponseOAuthToken, diff --git a/src/workos/sso/models/sso_provider_literal.py b/src/workos/sso/models/sso_provider_literal.py new file mode 100644 index 00000000..9395b5b8 --- /dev/null +++ b/src/workos/sso/models/sso_provider_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from .sso_provider import SSOProvider + +SSOProviderLiteral = SSOProvider +__all__ = ["SSOProviderLiteral"] diff --git a/src/workos/user_management/authentication/_resource.py b/src/workos/user_management/authentication/_resource.py index 55c01041..96ad7aa4 100644 --- a/src/workos/user_management/authentication/_resource.py +++ b/src/workos/user_management/authentication/_resource.py @@ -7,6 +7,7 @@ if TYPE_CHECKING: from ..._client import AsyncWorkOSClient, WorkOSClient +from ..._types import RequestOptions, enum_value from .models import ( AuthenticateResponse, AuthorizationCodeSessionAuthenticateRequest, @@ -23,7 +24,6 @@ UserManagementAuthenticationProvider, UserManagementAuthenticationScreenHint, ) -from ..._types import RequestOptions class UserManagementAuthentication: @@ -87,9 +87,11 @@ def authorize( provider_query_params: Optional[Dict[str, str]] = None, provider_scopes: Optional[List[str]] = None, invitation_token: Optional[str] = None, - screen_hint: Optional[UserManagementAuthenticationScreenHint] = None, + screen_hint: Optional[ + Union[UserManagementAuthenticationScreenHint, str] + ] = None, login_hint: Optional[str] = None, - provider: Optional[UserManagementAuthenticationProvider] = None, + provider: Optional[Union[UserManagementAuthenticationProvider, str]] = None, prompt: Optional[str] = None, state: Optional[str] = None, organization_id: Optional[str] = None, @@ -139,9 +141,11 @@ def authorize( "provider_query_params": provider_query_params, "provider_scopes": provider_scopes, "invitation_token": invitation_token, - "screen_hint": screen_hint.value if screen_hint else None, + "screen_hint": enum_value(screen_hint) + if screen_hint is not None + else None, "login_hint": login_hint, - "provider": provider.value if provider else None, + "provider": enum_value(provider) if provider is not None else None, "prompt": prompt, "state": state, "organization_id": organization_id, @@ -322,9 +326,11 @@ async def authorize( provider_query_params: Optional[Dict[str, str]] = None, provider_scopes: Optional[List[str]] = None, invitation_token: Optional[str] = None, - screen_hint: Optional[UserManagementAuthenticationScreenHint] = None, + screen_hint: Optional[ + Union[UserManagementAuthenticationScreenHint, str] + ] = None, login_hint: Optional[str] = None, - provider: Optional[UserManagementAuthenticationProvider] = None, + provider: Optional[Union[UserManagementAuthenticationProvider, str]] = None, prompt: Optional[str] = None, state: Optional[str] = None, organization_id: Optional[str] = None, @@ -374,9 +380,11 @@ async def authorize( "provider_query_params": provider_query_params, "provider_scopes": provider_scopes, "invitation_token": invitation_token, - "screen_hint": screen_hint.value if screen_hint else None, + "screen_hint": enum_value(screen_hint) + if screen_hint is not None + else None, "login_hint": login_hint, - "provider": provider.value if provider else None, + "provider": enum_value(provider) if provider is not None else None, "prompt": prompt, "state": state, "organization_id": organization_id, diff --git a/src/workos/user_management/authentication/models/__init__.py b/src/workos/user_management/authentication/models/__init__.py index b09cdf3c..1023abd0 100644 --- a/src/workos/user_management/authentication/models/__init__.py +++ b/src/workos/user_management/authentication/models/__init__.py @@ -23,7 +23,6 @@ from .sso_device_authorization_request import ( SSODeviceAuthorizationRequest as SSODeviceAuthorizationRequest, ) -from .screen_hint_type import ScreenHintType as ScreenHintType from .urn_ietf_params_oauth_grant_type_device_code_session_authenticate_request import ( UrnIetfParamsOAuthGrantTypeDeviceCodeSessionAuthenticateRequest as UrnIetfParamsOAuthGrantTypeDeviceCodeSessionAuthenticateRequest, ) @@ -43,6 +42,12 @@ from .user_management_authentication_provider import ( UserManagementAuthenticationProvider as UserManagementAuthenticationProvider, ) +from .user_management_authentication_provider_literal import ( + UserManagementAuthenticationProviderLiteral as UserManagementAuthenticationProviderLiteral, +) from .user_management_authentication_screen_hint import ( UserManagementAuthenticationScreenHint as UserManagementAuthenticationScreenHint, ) +from .user_management_authentication_screen_hint_literal import ( + UserManagementAuthenticationScreenHintLiteral as UserManagementAuthenticationScreenHintLiteral, +) diff --git a/src/workos/user_management/authentication/models/user_management_authentication_provider_literal.py b/src/workos/user_management/authentication/models/user_management_authentication_provider_literal.py new file mode 100644 index 00000000..5eb8a783 --- /dev/null +++ b/src/workos/user_management/authentication/models/user_management_authentication_provider_literal.py @@ -0,0 +1,8 @@ +# This file is auto-generated by oagen. Do not edit. + +from .user_management_authentication_provider import ( + UserManagementAuthenticationProvider, +) + +UserManagementAuthenticationProviderLiteral = UserManagementAuthenticationProvider +__all__ = ["UserManagementAuthenticationProviderLiteral"] diff --git a/src/workos/user_management/authentication/models/user_management_authentication_screen_hint_literal.py b/src/workos/user_management/authentication/models/user_management_authentication_screen_hint_literal.py new file mode 100644 index 00000000..3fb051f6 --- /dev/null +++ b/src/workos/user_management/authentication/models/user_management_authentication_screen_hint_literal.py @@ -0,0 +1,8 @@ +# This file is auto-generated by oagen. Do not edit. + +from .user_management_authentication_screen_hint import ( + UserManagementAuthenticationScreenHint, +) + +UserManagementAuthenticationScreenHintLiteral = UserManagementAuthenticationScreenHint +__all__ = ["UserManagementAuthenticationScreenHintLiteral"] diff --git a/src/workos/user_management/cors_origins/_resource.py b/src/workos/user_management/cors_origins/_resource.py index d7c19e9f..b94a2128 100644 --- a/src/workos/user_management/cors_origins/_resource.py +++ b/src/workos/user_management/cors_origins/_resource.py @@ -7,8 +7,8 @@ if TYPE_CHECKING: from ..._client import AsyncWorkOSClient, WorkOSClient -from .models import CORSOriginResponse from ..._types import RequestOptions +from .models import CORSOriginResponse class UserManagementCorsOrigins: diff --git a/src/workos/user_management/data_providers/_resource.py b/src/workos/user_management/data_providers/_resource.py index b3f92c6a..89c08b51 100644 --- a/src/workos/user_management/data_providers/_resource.py +++ b/src/workos/user_management/data_providers/_resource.py @@ -7,8 +7,8 @@ if TYPE_CHECKING: from ..._client import AsyncWorkOSClient, WorkOSClient -from .models import ConnectedAccount, DataIntegrationsListResponse from ..._types import RequestOptions +from .models import ConnectedAccount, DataIntegrationsListResponse class UserManagementDataProviders: diff --git a/src/workos/user_management/invitations/_resource.py b/src/workos/user_management/invitations/_resource.py index 767408a0..3bd5c4d8 100644 --- a/src/workos/user_management/invitations/_resource.py +++ b/src/workos/user_management/invitations/_resource.py @@ -2,11 +2,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import TYPE_CHECKING, Any, Dict, Optional, Union if TYPE_CHECKING: from ..._client import AsyncWorkOSClient, WorkOSClient +from ..._types import RequestOptions, enum_value from .models import Invitation, UserInvite from .models import UserManagementInvitationsOrder from workos.common.models import ( @@ -14,7 +15,6 @@ ResendUserInviteOptionsDtoLocale, ) from ..._pagination import AsyncPage, SyncPage -from ..._types import RequestOptions class UserManagementInvitations: @@ -29,7 +29,7 @@ def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[UserManagementInvitationsOrder] = None, + order: Optional[Union[UserManagementInvitationsOrder, str]] = None, organization_id: Optional[str] = None, email: Optional[str] = None, request_options: Optional[RequestOptions] = None, @@ -62,7 +62,7 @@ def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "organization_id": organization_id, "email": email, }.items() @@ -84,7 +84,7 @@ def create( role_slug: Optional[str] = None, expires_in_days: Optional[int] = None, inviter_user_id: Optional[str] = None, - locale: Optional[CreateUserInviteOptionsDtoLocale] = None, + locale: Optional[Union[CreateUserInviteOptionsDtoLocale, str]] = None, request_options: Optional[RequestOptions] = None, ) -> UserInvite: """Send an invitation @@ -119,7 +119,7 @@ def create( "role_slug": role_slug, "expires_in_days": expires_in_days, "inviter_user_id": inviter_user_id, - "locale": locale, + "locale": enum_value(locale) if locale is not None else None, }.items() if v is not None } @@ -226,7 +226,7 @@ def resend( self, id: str, *, - locale: Optional[ResendUserInviteOptionsDtoLocale] = None, + locale: Optional[Union[ResendUserInviteOptionsDtoLocale, str]] = None, request_options: Optional[RequestOptions] = None, ) -> UserInvite: """Resend an invitation @@ -252,7 +252,7 @@ def resend( body: Dict[str, Any] = { k: v for k, v in { - "locale": locale, + "locale": enum_value(locale) if locale is not None else None, }.items() if v is not None } @@ -307,7 +307,7 @@ async def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[UserManagementInvitationsOrder] = None, + order: Optional[Union[UserManagementInvitationsOrder, str]] = None, organization_id: Optional[str] = None, email: Optional[str] = None, request_options: Optional[RequestOptions] = None, @@ -340,7 +340,7 @@ async def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "organization_id": organization_id, "email": email, }.items() @@ -362,7 +362,7 @@ async def create( role_slug: Optional[str] = None, expires_in_days: Optional[int] = None, inviter_user_id: Optional[str] = None, - locale: Optional[CreateUserInviteOptionsDtoLocale] = None, + locale: Optional[Union[CreateUserInviteOptionsDtoLocale, str]] = None, request_options: Optional[RequestOptions] = None, ) -> UserInvite: """Send an invitation @@ -397,7 +397,7 @@ async def create( "role_slug": role_slug, "expires_in_days": expires_in_days, "inviter_user_id": inviter_user_id, - "locale": locale, + "locale": enum_value(locale) if locale is not None else None, }.items() if v is not None } @@ -504,7 +504,7 @@ async def resend( self, id: str, *, - locale: Optional[ResendUserInviteOptionsDtoLocale] = None, + locale: Optional[Union[ResendUserInviteOptionsDtoLocale, str]] = None, request_options: Optional[RequestOptions] = None, ) -> UserInvite: """Resend an invitation @@ -530,7 +530,7 @@ async def resend( body: Dict[str, Any] = { k: v for k, v in { - "locale": locale, + "locale": enum_value(locale) if locale is not None else None, }.items() if v is not None } diff --git a/src/workos/user_management/jwt_template/_resource.py b/src/workos/user_management/jwt_template/_resource.py index 9139c0b4..ebbc0ecd 100644 --- a/src/workos/user_management/jwt_template/_resource.py +++ b/src/workos/user_management/jwt_template/_resource.py @@ -7,8 +7,8 @@ if TYPE_CHECKING: from ..._client import AsyncWorkOSClient, WorkOSClient -from .models import JWTTemplateResponse from ..._types import RequestOptions +from .models import JWTTemplateResponse class UserManagementJWTTemplate: diff --git a/src/workos/user_management/magic_auth/_resource.py b/src/workos/user_management/magic_auth/_resource.py index a62b7c3a..3415cd0b 100644 --- a/src/workos/user_management/magic_auth/_resource.py +++ b/src/workos/user_management/magic_auth/_resource.py @@ -7,8 +7,8 @@ if TYPE_CHECKING: from ..._client import AsyncWorkOSClient, WorkOSClient -from .models import MagicAuth from ..._types import RequestOptions +from .models import MagicAuth class UserManagementMagicAuth: diff --git a/src/workos/user_management/multi_factor_authentication/_resource.py b/src/workos/user_management/multi_factor_authentication/_resource.py index d83021af..50c46d3c 100644 --- a/src/workos/user_management/multi_factor_authentication/_resource.py +++ b/src/workos/user_management/multi_factor_authentication/_resource.py @@ -2,16 +2,16 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, Literal, Optional +from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Union if TYPE_CHECKING: from ..._client import AsyncWorkOSClient, WorkOSClient +from ..._types import RequestOptions, enum_value from .models import UserAuthenticationFactorEnrollResponse from workos.multi_factor_auth.models import AuthenticationFactor from .models import UserManagementMultiFactorAuthenticationOrder from ..._pagination import AsyncPage, SyncPage -from ..._types import RequestOptions class UserManagementMultiFactorAuthentication: @@ -27,7 +27,9 @@ def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[UserManagementMultiFactorAuthenticationOrder] = None, + order: Optional[ + Union[UserManagementMultiFactorAuthenticationOrder, str] + ] = None, request_options: Optional[RequestOptions] = None, ) -> SyncPage[AuthenticationFactor]: """List authentication factors @@ -57,7 +59,7 @@ def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } @@ -132,7 +134,9 @@ async def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[UserManagementMultiFactorAuthenticationOrder] = None, + order: Optional[ + Union[UserManagementMultiFactorAuthenticationOrder, str] + ] = None, request_options: Optional[RequestOptions] = None, ) -> AsyncPage[AuthenticationFactor]: """List authentication factors @@ -162,7 +166,7 @@ async def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } diff --git a/src/workos/user_management/organization_membership/_resource.py b/src/workos/user_management/organization_membership/_resource.py index 52e015c2..9eb23de9 100644 --- a/src/workos/user_management/organization_membership/_resource.py +++ b/src/workos/user_management/organization_membership/_resource.py @@ -2,18 +2,18 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union if TYPE_CHECKING: from ..._client import AsyncWorkOSClient, WorkOSClient +from ..._types import RequestOptions, enum_value from .models import OrganizationMembership, UserOrganizationMembership from .models import ( UserManagementOrganizationMembershipOrder, UserManagementOrganizationMembershipStatuses, ) from ..._pagination import AsyncPage, SyncPage -from ..._types import RequestOptions class UserManagementOrganizationMembership: @@ -28,9 +28,11 @@ def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[UserManagementOrganizationMembershipOrder] = None, + order: Optional[Union[UserManagementOrganizationMembershipOrder, str]] = None, organization_id: Optional[str] = None, - statuses: Optional[List[UserManagementOrganizationMembershipStatuses]] = None, + statuses: Optional[ + List[Union[UserManagementOrganizationMembershipStatuses, str]] + ] = None, user_id: Optional[str] = None, request_options: Optional[RequestOptions] = None, ) -> SyncPage[UserOrganizationMembership]: @@ -65,7 +67,7 @@ def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "organization_id": organization_id, "statuses": statuses, "user_id": user_id, @@ -318,9 +320,11 @@ async def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[UserManagementOrganizationMembershipOrder] = None, + order: Optional[Union[UserManagementOrganizationMembershipOrder, str]] = None, organization_id: Optional[str] = None, - statuses: Optional[List[UserManagementOrganizationMembershipStatuses]] = None, + statuses: Optional[ + List[Union[UserManagementOrganizationMembershipStatuses, str]] + ] = None, user_id: Optional[str] = None, request_options: Optional[RequestOptions] = None, ) -> AsyncPage[UserOrganizationMembership]: @@ -355,7 +359,7 @@ async def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "organization_id": organization_id, "statuses": statuses, "user_id": user_id, diff --git a/src/workos/user_management/organization_membership/models/__init__.py b/src/workos/user_management/organization_membership/models/__init__.py index 6d359fd9..8a5a4ffc 100644 --- a/src/workos/user_management/organization_membership/models/__init__.py +++ b/src/workos/user_management/organization_membership/models/__init__.py @@ -4,6 +4,9 @@ CreateUserOrganizationMembership as CreateUserOrganizationMembership, ) from .organization_membership import OrganizationMembership as OrganizationMembership +from .organization_membership_status_literal import ( + OrganizationMembershipStatusLiteral as OrganizationMembershipStatusLiteral, +) from .update_user_organization_membership import ( UpdateUserOrganizationMembership as UpdateUserOrganizationMembership, ) diff --git a/src/workos/user_management/organization_membership/models/organization_membership_status_literal.py b/src/workos/user_management/organization_membership/models/organization_membership_status_literal.py new file mode 100644 index 00000000..dd07c587 --- /dev/null +++ b/src/workos/user_management/organization_membership/models/organization_membership_status_literal.py @@ -0,0 +1,6 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.common.models import OrganizationMembershipStatus + +OrganizationMembershipStatusLiteral = OrganizationMembershipStatus +__all__ = ["OrganizationMembershipStatusLiteral"] diff --git a/src/workos/user_management/redirect_uris/_resource.py b/src/workos/user_management/redirect_uris/_resource.py index b6dcc7b3..1848d7f2 100644 --- a/src/workos/user_management/redirect_uris/_resource.py +++ b/src/workos/user_management/redirect_uris/_resource.py @@ -7,8 +7,8 @@ if TYPE_CHECKING: from ..._client import AsyncWorkOSClient, WorkOSClient -from .models import RedirectUri from ..._types import RequestOptions +from .models import RedirectUri class UserManagementRedirectUris: diff --git a/src/workos/user_management/session_tokens/_resource.py b/src/workos/user_management/session_tokens/_resource.py index 4dd36804..2fee3037 100644 --- a/src/workos/user_management/session_tokens/_resource.py +++ b/src/workos/user_management/session_tokens/_resource.py @@ -7,8 +7,8 @@ if TYPE_CHECKING: from ..._client import AsyncWorkOSClient, WorkOSClient -from .models import JwksResponse from ..._types import RequestOptions +from .models import JwksResponse class UserManagementSessionTokens: diff --git a/src/workos/user_management/users/_resource.py b/src/workos/user_management/users/_resource.py index 6775b337..5079e86d 100644 --- a/src/workos/user_management/users/_resource.py +++ b/src/workos/user_management/users/_resource.py @@ -2,11 +2,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union, cast if TYPE_CHECKING: from ..._client import AsyncWorkOSClient, WorkOSClient +from ..._types import RequestOptions, enum_value from .models import ( EmailVerification, PasswordReset, @@ -23,7 +24,6 @@ UpdateUserDtoPasswordHashType, ) from ..._pagination import AsyncPage, SyncPage -from ..._types import RequestOptions class UserManagementUsers: @@ -174,7 +174,7 @@ def list_users( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[UserManagementUsersOrder] = None, + order: Optional[Union[UserManagementUsersOrder, str]] = None, organization: Optional[str] = None, organization_id: Optional[str] = None, email: Optional[str] = None, @@ -209,7 +209,7 @@ def list_users( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "organization": organization, "organization_id": organization_id, "email": email, @@ -230,7 +230,7 @@ def create( email: str, password: Optional[str] = None, password_hash: Optional[str] = None, - password_hash_type: Optional[CreateUserDtoPasswordHashType] = None, + password_hash_type: Optional[Union[CreateUserDtoPasswordHashType, str]] = None, first_name: Optional[str] = None, last_name: Optional[str] = None, email_verified: Optional[bool] = None, @@ -271,7 +271,9 @@ def create( "email": email, "password": password, "password_hash": password_hash, - "password_hash_type": password_hash_type, + "password_hash_type": enum_value(password_hash_type) + if password_hash_type is not None + else None, "first_name": first_name, "last_name": last_name, "email_verified": email_verified, @@ -358,7 +360,7 @@ def update( email_verified: Optional[bool] = None, password: Optional[str] = None, password_hash: Optional[str] = None, - password_hash_type: Optional[UpdateUserDtoPasswordHashType] = None, + password_hash_type: Optional[Union[UpdateUserDtoPasswordHashType, str]] = None, metadata: Optional[Dict[str, str]] = None, external_id: Optional[str] = None, locale: Optional[str] = None, @@ -401,7 +403,9 @@ def update( "email_verified": email_verified, "password": password, "password_hash": password_hash, - "password_hash_type": password_hash_type, + "password_hash_type": enum_value(password_hash_type) + if password_hash_type is not None + else None, "metadata": metadata, "external_id": external_id, "locale": locale, @@ -551,7 +555,7 @@ def list_sessions( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[UserManagementUsersOrder] = None, + order: Optional[Union[UserManagementUsersOrder, str]] = None, request_options: Optional[RequestOptions] = None, ) -> SyncPage[UserSessionsListItem]: """List sessions @@ -582,7 +586,7 @@ def list_sessions( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } @@ -743,7 +747,7 @@ async def list_users( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[UserManagementUsersOrder] = None, + order: Optional[Union[UserManagementUsersOrder, str]] = None, organization: Optional[str] = None, organization_id: Optional[str] = None, email: Optional[str] = None, @@ -778,7 +782,7 @@ async def list_users( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, "organization": organization, "organization_id": organization_id, "email": email, @@ -799,7 +803,7 @@ async def create( email: str, password: Optional[str] = None, password_hash: Optional[str] = None, - password_hash_type: Optional[CreateUserDtoPasswordHashType] = None, + password_hash_type: Optional[Union[CreateUserDtoPasswordHashType, str]] = None, first_name: Optional[str] = None, last_name: Optional[str] = None, email_verified: Optional[bool] = None, @@ -840,7 +844,9 @@ async def create( "email": email, "password": password, "password_hash": password_hash, - "password_hash_type": password_hash_type, + "password_hash_type": enum_value(password_hash_type) + if password_hash_type is not None + else None, "first_name": first_name, "last_name": last_name, "email_verified": email_verified, @@ -927,7 +933,7 @@ async def update( email_verified: Optional[bool] = None, password: Optional[str] = None, password_hash: Optional[str] = None, - password_hash_type: Optional[UpdateUserDtoPasswordHashType] = None, + password_hash_type: Optional[Union[UpdateUserDtoPasswordHashType, str]] = None, metadata: Optional[Dict[str, str]] = None, external_id: Optional[str] = None, locale: Optional[str] = None, @@ -970,7 +976,9 @@ async def update( "email_verified": email_verified, "password": password, "password_hash": password_hash, - "password_hash_type": password_hash_type, + "password_hash_type": enum_value(password_hash_type) + if password_hash_type is not None + else None, "metadata": metadata, "external_id": external_id, "locale": locale, @@ -1120,7 +1128,7 @@ async def list_sessions( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[UserManagementUsersOrder] = None, + order: Optional[Union[UserManagementUsersOrder, str]] = None, request_options: Optional[RequestOptions] = None, ) -> AsyncPage[UserSessionsListItem]: """List sessions @@ -1151,7 +1159,7 @@ async def list_sessions( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } diff --git a/src/workos/user_management_users/authorized_applications/_resource.py b/src/workos/user_management_users/authorized_applications/_resource.py index ee207fff..f6f11c20 100644 --- a/src/workos/user_management_users/authorized_applications/_resource.py +++ b/src/workos/user_management_users/authorized_applications/_resource.py @@ -2,15 +2,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Union if TYPE_CHECKING: from ..._client import AsyncWorkOSClient, WorkOSClient +from ..._types import RequestOptions, enum_value from .models import AuthorizedConnectApplicationListData from .models import UserManagementUsersAuthorizedApplicationsOrder from ..._pagination import AsyncPage, SyncPage -from ..._types import RequestOptions class UserManagementUsersAuthorizedApplications: @@ -26,7 +26,9 @@ def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[UserManagementUsersAuthorizedApplicationsOrder] = None, + order: Optional[ + Union[UserManagementUsersAuthorizedApplicationsOrder, str] + ] = None, request_options: Optional[RequestOptions] = None, ) -> SyncPage[AuthorizedConnectApplicationListData]: """List authorized applications @@ -57,7 +59,7 @@ def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } @@ -111,7 +113,9 @@ async def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[UserManagementUsersAuthorizedApplicationsOrder] = None, + order: Optional[ + Union[UserManagementUsersAuthorizedApplicationsOrder, str] + ] = None, request_options: Optional[RequestOptions] = None, ) -> AsyncPage[AuthorizedConnectApplicationListData]: """List authorized applications @@ -142,7 +146,7 @@ async def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } diff --git a/src/workos/user_management_users/feature_flags/_resource.py b/src/workos/user_management_users/feature_flags/_resource.py index b0a7f4e5..5289cb41 100644 --- a/src/workos/user_management_users/feature_flags/_resource.py +++ b/src/workos/user_management_users/feature_flags/_resource.py @@ -2,15 +2,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Union if TYPE_CHECKING: from ..._client import AsyncWorkOSClient, WorkOSClient +from ..._types import RequestOptions, enum_value from workos.feature_flags.models import Flag from .models import UserManagementUsersFeatureFlagsOrder from ..._pagination import AsyncPage, SyncPage -from ..._types import RequestOptions class UserManagementUsersFeatureFlags: @@ -26,7 +26,7 @@ def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[UserManagementUsersFeatureFlagsOrder] = None, + order: Optional[Union[UserManagementUsersFeatureFlagsOrder, str]] = None, request_options: Optional[RequestOptions] = None, ) -> SyncPage[Flag]: """List enabled feature flags for a user @@ -56,7 +56,7 @@ def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } @@ -82,7 +82,7 @@ async def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[UserManagementUsersFeatureFlagsOrder] = None, + order: Optional[Union[UserManagementUsersFeatureFlagsOrder, str]] = None, request_options: Optional[RequestOptions] = None, ) -> AsyncPage[Flag]: """List enabled feature flags for a user @@ -112,7 +112,7 @@ async def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } diff --git a/src/workos/webhooks/_resource.py b/src/workos/webhooks/_resource.py index 3f040eca..c72ebd61 100644 --- a/src/workos/webhooks/_resource.py +++ b/src/workos/webhooks/_resource.py @@ -7,6 +7,7 @@ if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient +from .._types import RequestOptions, enum_value from .models import WebhookEndpointJson from .models import WebhooksOrder from workos.common.models import ( @@ -15,7 +16,6 @@ UpdateWebhookEndpointDtoStatus, ) from .._pagination import AsyncPage, SyncPage -from .._types import RequestOptions import hashlib import hmac import json @@ -34,7 +34,7 @@ def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[WebhooksOrder] = None, + order: Optional[Union[WebhooksOrder, str]] = None, request_options: Optional[RequestOptions] = None, ) -> SyncPage[WebhookEndpointJson]: """List Webhook Endpoints @@ -62,7 +62,7 @@ def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } @@ -78,7 +78,7 @@ def create( self, *, endpoint_url: str, - events: List[CreateWebhookEndpointDtoEvents], + events: List[Union[CreateWebhookEndpointDtoEvents, str]], request_options: Optional[RequestOptions] = None, ) -> WebhookEndpointJson: """Create a Webhook Endpoint @@ -117,8 +117,8 @@ def update( id: str, *, endpoint_url: Optional[str] = None, - status: Optional[UpdateWebhookEndpointDtoStatus] = None, - events: Optional[List[UpdateWebhookEndpointDtoEvents]] = None, + status: Optional[Union[UpdateWebhookEndpointDtoStatus, str]] = None, + events: Optional[List[Union[UpdateWebhookEndpointDtoEvents, str]]] = None, request_options: Optional[RequestOptions] = None, ) -> WebhookEndpointJson: """Update a Webhook Endpoint @@ -147,7 +147,7 @@ def update( k: v for k, v in { "endpoint_url": endpoint_url, - "status": status, + "status": enum_value(status) if status is not None else None, "events": events, }.items() if v is not None @@ -289,7 +289,7 @@ async def list( limit: Optional[int] = None, before: Optional[str] = None, after: Optional[str] = None, - order: Optional[WebhooksOrder] = None, + order: Optional[Union[WebhooksOrder, str]] = None, request_options: Optional[RequestOptions] = None, ) -> AsyncPage[WebhookEndpointJson]: """List Webhook Endpoints @@ -317,7 +317,7 @@ async def list( "limit": limit, "before": before, "after": after, - "order": order.value if order else None, + "order": enum_value(order) if order is not None else None, }.items() if v is not None } @@ -333,7 +333,7 @@ async def create( self, *, endpoint_url: str, - events: List[CreateWebhookEndpointDtoEvents], + events: List[Union[CreateWebhookEndpointDtoEvents, str]], request_options: Optional[RequestOptions] = None, ) -> WebhookEndpointJson: """Create a Webhook Endpoint @@ -372,8 +372,8 @@ async def update( id: str, *, endpoint_url: Optional[str] = None, - status: Optional[UpdateWebhookEndpointDtoStatus] = None, - events: Optional[List[UpdateWebhookEndpointDtoEvents]] = None, + status: Optional[Union[UpdateWebhookEndpointDtoStatus, str]] = None, + events: Optional[List[Union[UpdateWebhookEndpointDtoEvents, str]]] = None, request_options: Optional[RequestOptions] = None, ) -> WebhookEndpointJson: """Update a Webhook Endpoint @@ -402,7 +402,7 @@ async def update( k: v for k, v in { "endpoint_url": endpoint_url, - "status": status, + "status": enum_value(status) if status is not None else None, "events": events, }.items() if v is not None diff --git a/src/workos/widgets/_resource.py b/src/workos/widgets/_resource.py index 530ee6b2..6ac43be3 100644 --- a/src/workos/widgets/_resource.py +++ b/src/workos/widgets/_resource.py @@ -2,14 +2,14 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient +from .._types import RequestOptions from .models import WidgetSessionTokenResponse from workos.common.models import WidgetSessionTokenDtoScopes -from .._types import RequestOptions class Widgets: @@ -23,7 +23,7 @@ def issue_widget_session_token( *, organization_id: str, user_id: Optional[str] = None, - scopes: Optional[List[WidgetSessionTokenDtoScopes]] = None, + scopes: Optional[List[Union[WidgetSessionTokenDtoScopes, str]]] = None, request_options: Optional[RequestOptions] = None, ) -> WidgetSessionTokenResponse: """Generate a widget token @@ -76,7 +76,7 @@ async def issue_widget_session_token( *, organization_id: str, user_id: Optional[str] = None, - scopes: Optional[List[WidgetSessionTokenDtoScopes]] = None, + scopes: Optional[List[Union[WidgetSessionTokenDtoScopes, str]]] = None, request_options: Optional[RequestOptions] = None, ) -> WidgetSessionTokenResponse: """Generate a widget token diff --git a/src/workos/workos_connect/_resource.py b/src/workos/workos_connect/_resource.py index 9e365aa0..99c7f371 100644 --- a/src/workos/workos_connect/_resource.py +++ b/src/workos/workos_connect/_resource.py @@ -7,8 +7,8 @@ if TYPE_CHECKING: from .._client import AsyncWorkOSClient, WorkOSClient -from .models import ExternalAuthCompleteResponse, UserConsentOption, UserObject from .._types import RequestOptions +from .models import ExternalAuthCompleteResponse, UserConsentOption, UserObject class WorkosConnect: diff --git a/tests/fixtures/event.json b/tests/fixtures/event.json index 119bae22..0967ef42 100644 --- a/tests/fixtures/event.json +++ b/tests/fixtures/event.json @@ -1,29 +1 @@ -{ - "object": "event", - "id": "event_01EHZNVPK3SFK441A1RGBFSHRT", - "event": "dsync.user.created", - "data": { - "id": "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ", - "state": "active", - "emails": [ - { - "primary": true, - "value": "veda@foo-corp.com" - } - ], - "idp_id": "2836", - "object": "directory_user", - "username": "veda@foo-corp.com", - "last_name": "Torp", - "first_name": "Veda", - "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", - "raw_attributes": {}, - "custom_attributes": {}, - "created_at": "2021-06-25T19:07:33.155Z", - "updated_at": "2021-06-25T19:07:33.155Z" - }, - "created_at": "2026-01-15T12:00:00.000Z", - "context": { - "key": {} - } -} +{} diff --git a/tests/fixtures/list_event.json b/tests/fixtures/list_event.json index f44d25df..33babb6e 100644 --- a/tests/fixtures/list_event.json +++ b/tests/fixtures/list_event.json @@ -1,34 +1,6 @@ { "data": [ - { - "object": "event", - "id": "event_01EHZNVPK3SFK441A1RGBFSHRT", - "event": "dsync.user.created", - "data": { - "id": "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ", - "state": "active", - "emails": [ - { - "primary": true, - "value": "veda@foo-corp.com" - } - ], - "idp_id": "2836", - "object": "directory_user", - "username": "veda@foo-corp.com", - "last_name": "Torp", - "first_name": "Veda", - "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", - "raw_attributes": {}, - "custom_attributes": {}, - "created_at": "2021-06-25T19:07:33.155Z", - "updated_at": "2021-06-25T19:07:33.155Z" - }, - "created_at": "2026-01-15T12:00:00.000Z", - "context": { - "key": {} - } - } + {} ], "list_metadata": { "before": null, diff --git a/tests/test_admin_portal.py b/tests/test_admin_portal.py index 9eac10d6..8b10c073 100644 --- a/tests/test_admin_portal.py +++ b/tests/test_admin_portal.py @@ -16,11 +16,11 @@ class TestAdminPortal: - def test_create(self, workos, httpx_mock): + def test_generate_link(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("portal_link_response.json"), ) - result = workos.admin_portal.create(organization="test_organization") + result = workos.admin_portal.generate_link(organization="test_organization") assert isinstance(result, PortalLinkResponse) assert ( result.link @@ -32,24 +32,24 @@ def test_create(self, workos, httpx_mock): body = json.loads(request.content) assert body["organization"] == "test_organization" - def test_create_unauthorized(self, workos, httpx_mock): + def test_generate_link_unauthorized(self, workos, httpx_mock): httpx_mock.add_response( status_code=401, json={"message": "Unauthorized"}, ) with pytest.raises(AuthenticationException): - workos.admin_portal.create(organization="test_organization") + workos.admin_portal.generate_link(organization="test_organization") - def test_create_not_found(self, httpx_mock): + def test_generate_link_not_found(self, httpx_mock): workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) try: httpx_mock.add_response(status_code=404, json={"message": "Not found"}) with pytest.raises(NotFoundException): - workos.admin_portal.create(organization="test_organization") + workos.admin_portal.generate_link(organization="test_organization") finally: workos.close() - def test_create_rate_limited(self, httpx_mock): + def test_generate_link_rate_limited(self, httpx_mock): workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) try: httpx_mock.add_response( @@ -58,25 +58,25 @@ def test_create_rate_limited(self, httpx_mock): json={"message": "Slow down"}, ) with pytest.raises(RateLimitExceededException): - workos.admin_portal.create(organization="test_organization") + workos.admin_portal.generate_link(organization="test_organization") finally: workos.close() - def test_create_server_error(self, httpx_mock): + def test_generate_link_server_error(self, httpx_mock): workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) try: httpx_mock.add_response(status_code=500, json={"message": "Server error"}) with pytest.raises(ServerException): - workos.admin_portal.create(organization="test_organization") + workos.admin_portal.generate_link(organization="test_organization") finally: workos.close() @pytest.mark.asyncio class TestAsyncAdminPortal: - async def test_create(self, async_workos, httpx_mock): + async def test_generate_link(self, async_workos, httpx_mock): httpx_mock.add_response(json=load_fixture("portal_link_response.json")) - result = await async_workos.admin_portal.create( + result = await async_workos.admin_portal.generate_link( organization="test_organization" ) assert isinstance(result, PortalLinkResponse) @@ -88,23 +88,27 @@ async def test_create(self, async_workos, httpx_mock): assert request.method == "POST" assert request.url.path.endswith("/portal/generate_link") - async def test_create_unauthorized(self, async_workos, httpx_mock): + async def test_generate_link_unauthorized(self, async_workos, httpx_mock): httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) with pytest.raises(AuthenticationException): - await async_workos.admin_portal.create(organization="test_organization") + await async_workos.admin_portal.generate_link( + organization="test_organization" + ) - async def test_create_not_found(self, httpx_mock): + async def test_generate_link_not_found(self, httpx_mock): workos = AsyncWorkOS( api_key="sk_test_123", client_id="client_test", max_retries=0 ) try: httpx_mock.add_response(status_code=404, json={"message": "Not found"}) with pytest.raises(NotFoundException): - await workos.admin_portal.create(organization="test_organization") + await workos.admin_portal.generate_link( + organization="test_organization" + ) finally: await workos.close() - async def test_create_rate_limited(self, httpx_mock): + async def test_generate_link_rate_limited(self, httpx_mock): workos = AsyncWorkOS( api_key="sk_test_123", client_id="client_test", max_retries=0 ) @@ -115,17 +119,21 @@ async def test_create_rate_limited(self, httpx_mock): json={"message": "Slow down"}, ) with pytest.raises(RateLimitExceededException): - await workos.admin_portal.create(organization="test_organization") + await workos.admin_portal.generate_link( + organization="test_organization" + ) finally: await workos.close() - async def test_create_server_error(self, httpx_mock): + async def test_generate_link_server_error(self, httpx_mock): workos = AsyncWorkOS( api_key="sk_test_123", client_id="client_test", max_retries=0 ) try: httpx_mock.add_response(status_code=500, json={"message": "Server error"}) with pytest.raises(ServerException): - await workos.admin_portal.create(organization="test_organization") + await workos.admin_portal.generate_link( + organization="test_organization" + ) finally: await workos.close() diff --git a/tests/test_audit_logs.py b/tests/test_audit_logs.py index 0f497d5b..179f3216 100644 --- a/tests/test_audit_logs.py +++ b/tests/test_audit_logs.py @@ -51,23 +51,23 @@ def test_list_encodes_query_params(self, workos, httpx_mock): assert request.url.params["after"] == "cursor/after" assert request.url.params["order"] == "normal" - def test_list_schemas(self, workos, httpx_mock): + def test_schemas(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("list_audit_log_schema_json.json"), ) - page = workos.audit_logs.list_schemas("test_actionName") + page = workos.audit_logs.schemas("test_actionName") assert isinstance(page, SyncPage) assert isinstance(page.data, list) - def test_list_schemas_empty_page(self, workos, httpx_mock): + def test_schemas_empty_page(self, workos, httpx_mock): httpx_mock.add_response(json={"data": [], "list_metadata": {}}) - page = workos.audit_logs.list_schemas("test_actionName") + page = workos.audit_logs.schemas("test_actionName") assert isinstance(page, SyncPage) assert page.data == [] - def test_list_schemas_encodes_query_params(self, workos, httpx_mock): + def test_schemas_encodes_query_params(self, workos, httpx_mock): httpx_mock.add_response(json={"data": [], "list_metadata": {}}) - workos.audit_logs.list_schemas( + workos.audit_logs.schemas( "test_actionName", limit=10, before="cursor before", @@ -80,11 +80,11 @@ def test_list_schemas_encodes_query_params(self, workos, httpx_mock): assert request.url.params["after"] == "cursor/after" assert request.url.params["order"] == "normal" - def test_create_schema(self, workos, httpx_mock): + def test_create_schemas(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("audit_log_schema_json.json"), ) - result = workos.audit_logs.create_schema("test_actionName", targets=[]) + result = workos.audit_logs.create_schemas("test_actionName", targets=[]) assert isinstance(result, AuditLogSchemaJson) assert result.object == "audit_log_schema" assert result.version == 1 @@ -94,11 +94,11 @@ def test_create_schema(self, workos, httpx_mock): body = json.loads(request.content) assert "targets" in body - def test_create_event(self, workos, httpx_mock): + def test_create_events(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("audit_log_event_create_response.json"), ) - result = workos.audit_logs.create_event( + result = workos.audit_logs.create_events( organization_id="test_organization_id", event=AuditLogEvent.from_dict(load_fixture("audit_log_event.json")), ) @@ -211,21 +211,21 @@ async def test_list_encodes_query_params(self, async_workos, httpx_mock): assert request.url.params["after"] == "cursor/after" assert request.url.params["order"] == "normal" - async def test_list_schemas(self, async_workos, httpx_mock): + async def test_schemas(self, async_workos, httpx_mock): httpx_mock.add_response(json=load_fixture("list_audit_log_schema_json.json")) - page = await async_workos.audit_logs.list_schemas("test_actionName") + page = await async_workos.audit_logs.schemas("test_actionName") assert isinstance(page, AsyncPage) assert isinstance(page.data, list) - async def test_list_schemas_empty_page(self, async_workos, httpx_mock): + async def test_schemas_empty_page(self, async_workos, httpx_mock): httpx_mock.add_response(json={"data": [], "list_metadata": {}}) - page = await async_workos.audit_logs.list_schemas("test_actionName") + page = await async_workos.audit_logs.schemas("test_actionName") assert isinstance(page, AsyncPage) assert page.data == [] - async def test_list_schemas_encodes_query_params(self, async_workos, httpx_mock): + async def test_schemas_encodes_query_params(self, async_workos, httpx_mock): httpx_mock.add_response(json={"data": [], "list_metadata": {}}) - await async_workos.audit_logs.list_schemas( + await async_workos.audit_logs.schemas( "test_actionName", limit=10, before="cursor before", @@ -238,9 +238,9 @@ async def test_list_schemas_encodes_query_params(self, async_workos, httpx_mock) assert request.url.params["after"] == "cursor/after" assert request.url.params["order"] == "normal" - async def test_create_schema(self, async_workos, httpx_mock): + async def test_create_schemas(self, async_workos, httpx_mock): httpx_mock.add_response(json=load_fixture("audit_log_schema_json.json")) - result = await async_workos.audit_logs.create_schema( + result = await async_workos.audit_logs.create_schemas( "test_actionName", targets=[] ) assert isinstance(result, AuditLogSchemaJson) @@ -250,11 +250,11 @@ async def test_create_schema(self, async_workos, httpx_mock): assert request.method == "POST" assert request.url.path.endswith("/audit_logs/actions/test_actionName/schemas") - async def test_create_event(self, async_workos, httpx_mock): + async def test_create_events(self, async_workos, httpx_mock): httpx_mock.add_response( json=load_fixture("audit_log_event_create_response.json") ) - result = await async_workos.audit_logs.create_event( + result = await async_workos.audit_logs.create_events( organization_id="test_organization_id", event=AuditLogEvent.from_dict(load_fixture("audit_log_event.json")), ) diff --git a/tests/test_authorization.py b/tests/test_authorization.py index 0d3fce46..97f0f858 100644 --- a/tests/test_authorization.py +++ b/tests/test_authorization.py @@ -145,9 +145,9 @@ def test_assign_role(self, workos, httpx_mock): body = json.loads(request.content) assert body["role_slug"] == "test_role_slug" - def test_remove_role(self, workos, httpx_mock): + def test_remove_role_by_criteria(self, workos, httpx_mock): httpx_mock.add_response(status_code=204) - result = workos.authorization.remove_role( + result = workos.authorization.remove_role_by_criteria( "test_organization_membership_id", role_slug="test_role_slug" ) assert result is None @@ -169,11 +169,11 @@ def test_remove_role_by_id(self, workos, httpx_mock): "/authorization/organization_memberships/test_organization_membership_id/role_assignments/test_role_assignment_id" ) - def test_list_roles_organizations(self, workos, httpx_mock): + def test_list_organization_roles(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("list.json"), ) - result = workos.authorization.list_roles_organizations("test_organizationId") + result = workos.authorization.list_organization_roles("test_organizationId") assert isinstance(result, ListModel) assert result.object == "list" request = httpx_mock.get_request() @@ -182,11 +182,11 @@ def test_list_roles_organizations(self, workos, httpx_mock): "/authorization/organizations/test_organizationId/roles" ) - def test_create_roles_organizations(self, workos, httpx_mock): + def test_create_organization_role(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("role.json"), ) - result = workos.authorization.create_roles_organizations( + result = workos.authorization.create_organization_role( "test_organizationId", slug="test_slug", name="test_name" ) assert isinstance(result, Role) @@ -201,11 +201,11 @@ def test_create_roles_organizations(self, workos, httpx_mock): assert body["slug"] == "test_slug" assert body["name"] == "test_name" - def test_get_roles_organization(self, workos, httpx_mock): + def test_get_organization_role(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("role.json"), ) - result = workos.authorization.get_roles_organization( + result = workos.authorization.get_organization_role( "test_organizationId", "test_slug" ) assert isinstance(result, Role) @@ -217,11 +217,11 @@ def test_get_roles_organization(self, workos, httpx_mock): "/authorization/organizations/test_organizationId/roles/test_slug" ) - def test_update_roles_organizations(self, workos, httpx_mock): + def test_update_organization_role(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("role.json"), ) - result = workos.authorization.update_roles_organizations( + result = workos.authorization.update_organization_role( "test_organizationId", "test_slug" ) assert isinstance(result, Role) @@ -233,9 +233,11 @@ def test_update_roles_organizations(self, workos, httpx_mock): "/authorization/organizations/test_organizationId/roles/test_slug" ) - def test_delete_roles(self, workos, httpx_mock): + def test_delete_organization_role(self, workos, httpx_mock): httpx_mock.add_response(status_code=204) - result = workos.authorization.delete_roles("test_organizationId", "test_slug") + result = workos.authorization.delete_organization_role( + "test_organizationId", "test_slug" + ) assert result is None request = httpx_mock.get_request() assert request.method == "DELETE" @@ -446,11 +448,11 @@ def test_list_resources_encodes_query_params(self, workos, httpx_mock): ) assert request.url.params["search"] == "value search/test" - def test_create_resource(self, workos, httpx_mock): + def test_create_resources(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("authorization_resource.json"), ) - result = workos.authorization.create_resource( + result = workos.authorization.create_resources( external_id="test_external_id", name="test_name", resource_type_slug="test_resource_type_slug", @@ -480,11 +482,11 @@ def test_get_by_id(self, workos, httpx_mock): assert request.method == "GET" assert request.url.path.endswith("/authorization/resources/test_resource_id") - def test_update_resource(self, workos, httpx_mock): + def test_update_resources(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("authorization_resource.json"), ) - result = workos.authorization.update_resource("test_resource_id") + result = workos.authorization.update_resources("test_resource_id") assert isinstance(result, AuthorizationResource) assert result.object == "authorization_resource" assert result.name == "Website Redesign" @@ -492,17 +494,17 @@ def test_update_resource(self, workos, httpx_mock): assert request.method == "PATCH" assert request.url.path.endswith("/authorization/resources/test_resource_id") - def test_delete_resource(self, workos, httpx_mock): + def test_delete_resources(self, workos, httpx_mock): httpx_mock.add_response(status_code=204) - result = workos.authorization.delete_resource("test_resource_id") + result = workos.authorization.delete_resources("test_resource_id") assert result is None request = httpx_mock.get_request() assert request.method == "DELETE" assert request.url.path.endswith("/authorization/resources/test_resource_id") - def test_delete_resource_encodes_query_params(self, workos, httpx_mock): + def test_delete_resources_encodes_query_params(self, workos, httpx_mock): httpx_mock.add_response(status_code=204) - workos.authorization.delete_resource("test_resource_id", cascade_delete=True) + workos.authorization.delete_resources("test_resource_id", cascade_delete=True) request = httpx_mock.get_request() assert request.url.params["cascade_delete"] == "true" @@ -794,9 +796,9 @@ async def test_assign_role(self, async_workos, httpx_mock): "/authorization/organization_memberships/test_organization_membership_id/role_assignments" ) - async def test_remove_role(self, async_workos, httpx_mock): + async def test_remove_role_by_criteria(self, async_workos, httpx_mock): httpx_mock.add_response(status_code=204) - result = await async_workos.authorization.remove_role( + result = await async_workos.authorization.remove_role_by_criteria( "test_organization_membership_id", role_slug="test_role_slug" ) assert result is None @@ -818,9 +820,9 @@ async def test_remove_role_by_id(self, async_workos, httpx_mock): "/authorization/organization_memberships/test_organization_membership_id/role_assignments/test_role_assignment_id" ) - async def test_list_roles_organizations(self, async_workos, httpx_mock): + async def test_list_organization_roles(self, async_workos, httpx_mock): httpx_mock.add_response(json=load_fixture("list.json")) - result = await async_workos.authorization.list_roles_organizations( + result = await async_workos.authorization.list_organization_roles( "test_organizationId" ) assert isinstance(result, ListModel) @@ -831,9 +833,9 @@ async def test_list_roles_organizations(self, async_workos, httpx_mock): "/authorization/organizations/test_organizationId/roles" ) - async def test_create_roles_organizations(self, async_workos, httpx_mock): + async def test_create_organization_role(self, async_workos, httpx_mock): httpx_mock.add_response(json=load_fixture("role.json")) - result = await async_workos.authorization.create_roles_organizations( + result = await async_workos.authorization.create_organization_role( "test_organizationId", slug="test_slug", name="test_name" ) assert isinstance(result, Role) @@ -845,9 +847,9 @@ async def test_create_roles_organizations(self, async_workos, httpx_mock): "/authorization/organizations/test_organizationId/roles" ) - async def test_get_roles_organization(self, async_workos, httpx_mock): + async def test_get_organization_role(self, async_workos, httpx_mock): httpx_mock.add_response(json=load_fixture("role.json")) - result = await async_workos.authorization.get_roles_organization( + result = await async_workos.authorization.get_organization_role( "test_organizationId", "test_slug" ) assert isinstance(result, Role) @@ -859,9 +861,9 @@ async def test_get_roles_organization(self, async_workos, httpx_mock): "/authorization/organizations/test_organizationId/roles/test_slug" ) - async def test_update_roles_organizations(self, async_workos, httpx_mock): + async def test_update_organization_role(self, async_workos, httpx_mock): httpx_mock.add_response(json=load_fixture("role.json")) - result = await async_workos.authorization.update_roles_organizations( + result = await async_workos.authorization.update_organization_role( "test_organizationId", "test_slug" ) assert isinstance(result, Role) @@ -873,9 +875,9 @@ async def test_update_roles_organizations(self, async_workos, httpx_mock): "/authorization/organizations/test_organizationId/roles/test_slug" ) - async def test_delete_roles(self, async_workos, httpx_mock): + async def test_delete_organization_role(self, async_workos, httpx_mock): httpx_mock.add_response(status_code=204) - result = await async_workos.authorization.delete_roles( + result = await async_workos.authorization.delete_organization_role( "test_organizationId", "test_slug" ) assert result is None @@ -1080,9 +1082,9 @@ async def test_list_resources_encodes_query_params(self, async_workos, httpx_moc ) assert request.url.params["search"] == "value search/test" - async def test_create_resource(self, async_workos, httpx_mock): + async def test_create_resources(self, async_workos, httpx_mock): httpx_mock.add_response(json=load_fixture("authorization_resource.json")) - result = await async_workos.authorization.create_resource( + result = await async_workos.authorization.create_resources( external_id="test_external_id", name="test_name", resource_type_slug="test_resource_type_slug", @@ -1105,9 +1107,9 @@ async def test_get_by_id(self, async_workos, httpx_mock): assert request.method == "GET" assert request.url.path.endswith("/authorization/resources/test_resource_id") - async def test_update_resource(self, async_workos, httpx_mock): + async def test_update_resources(self, async_workos, httpx_mock): httpx_mock.add_response(json=load_fixture("authorization_resource.json")) - result = await async_workos.authorization.update_resource("test_resource_id") + result = await async_workos.authorization.update_resources("test_resource_id") assert isinstance(result, AuthorizationResource) assert result.object == "authorization_resource" assert result.name == "Website Redesign" @@ -1115,17 +1117,19 @@ async def test_update_resource(self, async_workos, httpx_mock): assert request.method == "PATCH" assert request.url.path.endswith("/authorization/resources/test_resource_id") - async def test_delete_resource(self, async_workos, httpx_mock): + async def test_delete_resources(self, async_workos, httpx_mock): httpx_mock.add_response(status_code=204) - result = await async_workos.authorization.delete_resource("test_resource_id") + result = await async_workos.authorization.delete_resources("test_resource_id") assert result is None request = httpx_mock.get_request() assert request.method == "DELETE" assert request.url.path.endswith("/authorization/resources/test_resource_id") - async def test_delete_resource_encodes_query_params(self, async_workos, httpx_mock): + async def test_delete_resources_encodes_query_params( + self, async_workos, httpx_mock + ): httpx_mock.add_response(status_code=204) - await async_workos.authorization.delete_resource( + await async_workos.authorization.delete_resources( "test_resource_id", cascade_delete=True ) request = httpx_mock.get_request() diff --git a/tests/test_events.py b/tests/test_events.py index 0e9239eb..278d5b16 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -16,23 +16,23 @@ class TestEvents: - def test_list_events(self, workos, httpx_mock): + def test_list(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("list_event.json"), ) - page = workos.events.list_events() + page = workos.events.list() assert isinstance(page, SyncPage) assert isinstance(page.data, list) - def test_list_events_empty_page(self, workos, httpx_mock): + def test_list_empty_page(self, workos, httpx_mock): httpx_mock.add_response(json={"data": [], "list_metadata": {}}) - page = workos.events.list_events() + page = workos.events.list() assert isinstance(page, SyncPage) assert page.data == [] - def test_list_events_encodes_query_params(self, workos, httpx_mock): + def test_list_encodes_query_params(self, workos, httpx_mock): httpx_mock.add_response(json={"data": [], "list_metadata": {}}) - workos.events.list_events( + workos.events.list( limit=10, before="cursor before", after="cursor/after", @@ -50,24 +50,24 @@ def test_list_events_encodes_query_params(self, workos, httpx_mock): assert request.url.params["range_end"] == "value range_end/test" assert request.url.params["organization_id"] == "value organization_id/test" - def test_list_events_unauthorized(self, workos, httpx_mock): + def test_list_unauthorized(self, workos, httpx_mock): httpx_mock.add_response( status_code=401, json={"message": "Unauthorized"}, ) with pytest.raises(AuthenticationException): - workos.events.list_events() + workos.events.list() - def test_list_events_not_found(self, httpx_mock): + def test_list_not_found(self, httpx_mock): workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) try: httpx_mock.add_response(status_code=404, json={"message": "Not found"}) with pytest.raises(NotFoundException): - workos.events.list_events() + workos.events.list() finally: workos.close() - def test_list_events_rate_limited(self, httpx_mock): + def test_list_rate_limited(self, httpx_mock): workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) try: httpx_mock.add_response( @@ -76,37 +76,37 @@ def test_list_events_rate_limited(self, httpx_mock): json={"message": "Slow down"}, ) with pytest.raises(RateLimitExceededException): - workos.events.list_events() + workos.events.list() finally: workos.close() - def test_list_events_server_error(self, httpx_mock): + def test_list_server_error(self, httpx_mock): workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0) try: httpx_mock.add_response(status_code=500, json={"message": "Server error"}) with pytest.raises(ServerException): - workos.events.list_events() + workos.events.list() finally: workos.close() @pytest.mark.asyncio class TestAsyncEvents: - async def test_list_events(self, async_workos, httpx_mock): + async def test_list(self, async_workos, httpx_mock): httpx_mock.add_response(json=load_fixture("list_event.json")) - page = await async_workos.events.list_events() + page = await async_workos.events.list() assert isinstance(page, AsyncPage) assert isinstance(page.data, list) - async def test_list_events_empty_page(self, async_workos, httpx_mock): + async def test_list_empty_page(self, async_workos, httpx_mock): httpx_mock.add_response(json={"data": [], "list_metadata": {}}) - page = await async_workos.events.list_events() + page = await async_workos.events.list() assert isinstance(page, AsyncPage) assert page.data == [] - async def test_list_events_encodes_query_params(self, async_workos, httpx_mock): + async def test_list_encodes_query_params(self, async_workos, httpx_mock): httpx_mock.add_response(json={"data": [], "list_metadata": {}}) - await async_workos.events.list_events( + await async_workos.events.list( limit=10, before="cursor before", after="cursor/after", @@ -124,23 +124,23 @@ async def test_list_events_encodes_query_params(self, async_workos, httpx_mock): assert request.url.params["range_end"] == "value range_end/test" assert request.url.params["organization_id"] == "value organization_id/test" - async def test_list_events_unauthorized(self, async_workos, httpx_mock): + async def test_list_unauthorized(self, async_workos, httpx_mock): httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"}) with pytest.raises(AuthenticationException): - await async_workos.events.list_events() + await async_workos.events.list() - async def test_list_events_not_found(self, httpx_mock): + async def test_list_not_found(self, httpx_mock): workos = AsyncWorkOS( api_key="sk_test_123", client_id="client_test", max_retries=0 ) try: httpx_mock.add_response(status_code=404, json={"message": "Not found"}) with pytest.raises(NotFoundException): - await workos.events.list_events() + await workos.events.list() finally: await workos.close() - async def test_list_events_rate_limited(self, httpx_mock): + async def test_list_rate_limited(self, httpx_mock): workos = AsyncWorkOS( api_key="sk_test_123", client_id="client_test", max_retries=0 ) @@ -151,17 +151,17 @@ async def test_list_events_rate_limited(self, httpx_mock): json={"message": "Slow down"}, ) with pytest.raises(RateLimitExceededException): - await workos.events.list_events() + await workos.events.list() finally: await workos.close() - async def test_list_events_server_error(self, httpx_mock): + async def test_list_server_error(self, httpx_mock): workos = AsyncWorkOS( api_key="sk_test_123", client_id="client_test", max_retries=0 ) try: httpx_mock.add_response(status_code=500, json={"message": "Server error"}) with pytest.raises(ServerException): - await workos.events.list_events() + await workos.events.list() finally: await workos.close() diff --git a/tests/test_models_round_trip.py b/tests/test_models_round_trip.py index e12f32ec..1f87de50 100644 --- a/tests/test_models_round_trip.py +++ b/tests/test_models_round_trip.py @@ -1630,69 +1630,98 @@ def test_directory_user_with_groups_round_trips_unknown_enum_values(self): instance = DirectoryUserWithGroups.from_dict(data) assert instance.to_dict() == data - def test_event_round_trip(self): - data = load_fixture("event.json") - instance = Event.from_dict(data) + def test_user_round_trip(self): + data = load_fixture("user.json") + instance = User.from_dict(data) serialized = instance.to_dict() assert serialized == data - restored = Event.from_dict(serialized) + restored = User.from_dict(serialized) assert restored.to_dict() == serialized - def test_event_minimal_payload(self): + def test_user_minimal_payload(self): data = { - "object": "event", - "id": "event_01EHZNVPK3SFK441A1RGBFSHRT", - "event": "dsync.user.created", - "data": { - "id": "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ", - "state": "active", - "emails": [{"primary": True, "value": "veda@foo-corp.com"}], - "idp_id": "2836", - "object": "directory_user", - "username": "veda@foo-corp.com", - "last_name": "Torp", - "first_name": "Veda", - "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", - "raw_attributes": {}, - "custom_attributes": {}, - "created_at": "2021-06-25T19:07:33.155Z", - "updated_at": "2021-06-25T19:07:33.155Z", - }, + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": None, + "last_name": None, + "profile_picture_url": None, + "email": "marcelina.davis@example.com", + "email_verified": True, + "external_id": None, + "last_sign_in_at": None, "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", } - instance = Event.from_dict(data) + instance = User.from_dict(data) serialized = instance.to_dict() assert serialized["object"] == data["object"] assert serialized["id"] == data["id"] - assert serialized["event"] == data["event"] - assert serialized["data"] == data["data"] + assert serialized["first_name"] == data["first_name"] + assert serialized["last_name"] == data["last_name"] + assert serialized["profile_picture_url"] == data["profile_picture_url"] + assert serialized["email"] == data["email"] + assert serialized["email_verified"] == data["email_verified"] + assert serialized["external_id"] == data["external_id"] + assert serialized["last_sign_in_at"] == data["last_sign_in_at"] assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] - def test_event_omits_absent_optional_non_nullable_fields(self): + def test_user_omits_absent_optional_non_nullable_fields(self): data = { - "object": "event", - "id": "event_01EHZNVPK3SFK441A1RGBFSHRT", - "event": "dsync.user.created", - "data": { - "id": "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ", - "state": "active", - "emails": [{"primary": True, "value": "veda@foo-corp.com"}], - "idp_id": "2836", - "object": "directory_user", - "username": "veda@foo-corp.com", - "last_name": "Torp", - "first_name": "Veda", - "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", - "raw_attributes": {}, - "custom_attributes": {}, - "created_at": "2021-06-25T19:07:33.155Z", - "updated_at": "2021-06-25T19:07:33.155Z", - }, + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": "Marcelina", + "last_name": "Davis", + "profile_picture_url": "https://workoscdn.com/images/v1/123abc", + "email": "marcelina.davis@example.com", + "email_verified": True, + "external_id": "f1ffa2b2-c20b-4d39-be5c-212726e11222", + "last_sign_in_at": "2025-06-25T19:07:33.155Z", + "locale": "en-US", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = User.from_dict(data) + serialized = instance.to_dict() + assert "metadata" not in serialized + + def test_user_preserves_nullable_fields(self): + data = { + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": None, + "last_name": None, + "profile_picture_url": None, + "email": "marcelina.davis@example.com", + "email_verified": True, + "external_id": None, + "metadata": {"timezone": "America/New_York"}, + "last_sign_in_at": None, + "locale": None, "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", } + instance = User.from_dict(data) + serialized = instance.to_dict() + assert serialized["first_name"] is None + assert serialized["last_name"] is None + assert serialized["profile_picture_url"] is None + assert serialized["external_id"] is None + assert serialized["last_sign_in_at"] is None + assert serialized["locale"] is None + + def test_event_round_trip(self): + data = load_fixture("event.json") instance = Event.from_dict(data) serialized = instance.to_dict() - assert "context" not in serialized + assert serialized == data + restored = Event.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_event_minimal_payload(self): + data = {} + instance = Event.from_dict(data) + assert instance.to_dict() is not None def test_jwt_template_response_round_trip(self): data = load_fixture("jwt_template_response.json") @@ -1913,7 +1942,6 @@ def test_organization_minimal_payload(self): "external_id": None, "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z", - "allow_profiles_outside_organization": False, } instance = Organization.from_dict(data) serialized = instance.to_dict() @@ -1925,10 +1953,6 @@ def test_organization_minimal_payload(self): assert serialized["external_id"] == data["external_id"] assert serialized["created_at"] == data["created_at"] assert serialized["updated_at"] == data["updated_at"] - assert ( - serialized["allow_profiles_outside_organization"] - == data["allow_profiles_outside_organization"] - ) def test_organization_omits_absent_optional_non_nullable_fields(self): data = { @@ -1953,11 +1977,11 @@ def test_organization_omits_absent_optional_non_nullable_fields(self): "external_id": "2fe01467-f7ea-4dd2-8b79-c2b4f56d0191", "created_at": "2026-01-15T12:00:00.000Z", "updated_at": "2026-01-15T12:00:00.000Z", - "allow_profiles_outside_organization": False, } instance = Organization.from_dict(data) serialized = instance.to_dict() assert "stripe_customer_id" not in serialized + assert "allow_profiles_outside_organization" not in serialized def test_organization_preserves_nullable_fields(self): data = { @@ -2499,86 +2523,6 @@ def test_user_organization_membership_round_trips_unknown_enum_values(self): instance = UserOrganizationMembership.from_dict(data) assert instance.to_dict() == data - def test_user_round_trip(self): - data = load_fixture("user.json") - instance = User.from_dict(data) - serialized = instance.to_dict() - assert serialized == data - restored = User.from_dict(serialized) - assert restored.to_dict() == serialized - - def test_user_minimal_payload(self): - data = { - "object": "user", - "id": "user_01E4ZCR3C56J083X43JQXF3JK5", - "first_name": None, - "last_name": None, - "profile_picture_url": None, - "email": "marcelina.davis@example.com", - "email_verified": True, - "external_id": None, - "last_sign_in_at": None, - "created_at": "2026-01-15T12:00:00.000Z", - "updated_at": "2026-01-15T12:00:00.000Z", - } - instance = User.from_dict(data) - serialized = instance.to_dict() - assert serialized["object"] == data["object"] - assert serialized["id"] == data["id"] - assert serialized["first_name"] == data["first_name"] - assert serialized["last_name"] == data["last_name"] - assert serialized["profile_picture_url"] == data["profile_picture_url"] - assert serialized["email"] == data["email"] - assert serialized["email_verified"] == data["email_verified"] - assert serialized["external_id"] == data["external_id"] - assert serialized["last_sign_in_at"] == data["last_sign_in_at"] - assert serialized["created_at"] == data["created_at"] - assert serialized["updated_at"] == data["updated_at"] - - def test_user_omits_absent_optional_non_nullable_fields(self): - data = { - "object": "user", - "id": "user_01E4ZCR3C56J083X43JQXF3JK5", - "first_name": "Marcelina", - "last_name": "Davis", - "profile_picture_url": "https://workoscdn.com/images/v1/123abc", - "email": "marcelina.davis@example.com", - "email_verified": True, - "external_id": "f1ffa2b2-c20b-4d39-be5c-212726e11222", - "last_sign_in_at": "2025-06-25T19:07:33.155Z", - "locale": "en-US", - "created_at": "2026-01-15T12:00:00.000Z", - "updated_at": "2026-01-15T12:00:00.000Z", - } - instance = User.from_dict(data) - serialized = instance.to_dict() - assert "metadata" not in serialized - - def test_user_preserves_nullable_fields(self): - data = { - "object": "user", - "id": "user_01E4ZCR3C56J083X43JQXF3JK5", - "first_name": None, - "last_name": None, - "profile_picture_url": None, - "email": "marcelina.davis@example.com", - "email_verified": True, - "external_id": None, - "metadata": {"timezone": "America/New_York"}, - "last_sign_in_at": None, - "locale": None, - "created_at": "2026-01-15T12:00:00.000Z", - "updated_at": "2026-01-15T12:00:00.000Z", - } - instance = User.from_dict(data) - serialized = instance.to_dict() - assert serialized["first_name"] is None - assert serialized["last_name"] is None - assert serialized["profile_picture_url"] is None - assert serialized["external_id"] is None - assert serialized["last_sign_in_at"] is None - assert serialized["locale"] is None - def test_email_verification_round_trip(self): data = load_fixture("email_verification.json") instance = EmailVerification.from_dict(data) diff --git a/tests/test_organization_domains.py b/tests/test_organization_domains.py index 352babab..08d4bfdb 100644 --- a/tests/test_organization_domains.py +++ b/tests/test_organization_domains.py @@ -56,11 +56,11 @@ def test_delete(self, workos, httpx_mock): assert request.method == "DELETE" assert request.url.path.endswith("/organization_domains/test_id") - def test_verify_organization_domain(self, workos, httpx_mock): + def test_verify(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("organization_domain_stand_alone.json"), ) - result = workos.organization_domains.verify_organization_domain("test_id") + result = workos.organization_domains.verify("test_id") assert isinstance(result, OrganizationDomainStandAlone) assert result.object == "organization_domain" assert result.id == "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A" @@ -150,13 +150,11 @@ async def test_delete(self, async_workos, httpx_mock): assert request.method == "DELETE" assert request.url.path.endswith("/organization_domains/test_id") - async def test_verify_organization_domain(self, async_workos, httpx_mock): + async def test_verify(self, async_workos, httpx_mock): httpx_mock.add_response( json=load_fixture("organization_domain_stand_alone.json") ) - result = await async_workos.organization_domains.verify_organization_domain( - "test_id" - ) + result = await async_workos.organization_domains.verify("test_id") assert isinstance(result, OrganizationDomainStandAlone) assert result.object == "organization_domain" assert result.id == "org_domain_01EHZNVPK2QXHMVWCEDQEKY69A" diff --git a/tests/test_sso.py b/tests/test_sso.py index b01ff00c..85ec773b 100644 --- a/tests/test_sso.py +++ b/tests/test_sso.py @@ -62,11 +62,11 @@ def test_get_profile(self, workos, httpx_mock): assert request.method == "GET" assert request.url.path.endswith("/sso/profile") - def test_get_profile_and_token(self, workos, httpx_mock): + def test_token(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("sso_token_response.json"), ) - result = workos.sso.get_profile_and_token( + result = workos.sso.token( client_id="test_client_id", client_secret="test_client_secret", code="test_code", @@ -166,9 +166,9 @@ async def test_get_profile(self, async_workos, httpx_mock): assert request.method == "GET" assert request.url.path.endswith("/sso/profile") - async def test_get_profile_and_token(self, async_workos, httpx_mock): + async def test_token(self, async_workos, httpx_mock): httpx_mock.add_response(json=load_fixture("sso_token_response.json")) - result = await async_workos.sso.get_profile_and_token( + result = await async_workos.sso.token( client_id="test_client_id", client_secret="test_client_secret", code="test_code", From b2b1fbf8a95f5db447d47b7a851bdc58e0c82ee2 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 31 Mar 2026 17:36:52 -0400 Subject: [PATCH 20/26] fix: improve method names, restore timeout, add Event fields Regenerated from updated emitter with: - Audit log exports: exports/export -> create_export/get_export - Authorization role permissions: unwieldy names replaced with set_organization_role_permissions, add_organization_role_permission, etc. - Default request timeout restored to 30s (was 25s) - Event model hand-maintained via oagen-ignore with id, event, data, created_at fields (emitter produces empty placeholder for oneOf unions) Co-Authored-By: Claude Opus 4.6 (1M context) --- src/workos/_client.py | 6 +-- src/workos/audit_logs/_resource.py | 24 ++++++++-- src/workos/authorization/_resource.py | 68 +++++++++++++++++++++++---- src/workos/events/models/event.py | 32 +++++++++++-- tests/fixtures/event.json | 10 +++- tests/fixtures/list_event.json | 10 +++- tests/test_audit_logs.py | 16 +++---- tests/test_authorization.py | 44 ++++++++--------- tests/test_models_round_trip.py | 2 +- 9 files changed, 155 insertions(+), 57 deletions(-) diff --git a/src/workos/_client.py b/src/workos/_client.py index 73b7e95f..f3f72751 100644 --- a/src/workos/_client.py +++ b/src/workos/_client.py @@ -420,7 +420,7 @@ def __init__( self._request_timeout = ( request_timeout if request_timeout is not None - else int(os.environ.get("WORKOS_REQUEST_TIMEOUT", "25")) + else int(os.environ.get("WORKOS_REQUEST_TIMEOUT", "30")) ) self._max_retries = max_retries self._jwt_leeway = jwt_leeway @@ -647,7 +647,7 @@ def __init__( api_key: WorkOS API key. Falls back to the WORKOS_API_KEY environment variable. client_id: WorkOS client ID. Falls back to the WORKOS_CLIENT_ID environment variable. base_url: Base URL for API requests. Falls back to WORKOS_BASE_URL or "https://api.workos.com". - request_timeout: HTTP request timeout in seconds. Falls back to WORKOS_REQUEST_TIMEOUT or 25. + request_timeout: HTTP request timeout in seconds. Falls back to WORKOS_REQUEST_TIMEOUT or 30. jwt_leeway: JWT clock skew leeway in seconds. max_retries: Maximum number of retries for failed requests. Defaults to 3. @@ -926,7 +926,7 @@ def __init__( api_key: WorkOS API key. Falls back to the WORKOS_API_KEY environment variable. client_id: WorkOS client ID. Falls back to the WORKOS_CLIENT_ID environment variable. base_url: Base URL for API requests. Falls back to WORKOS_BASE_URL or "https://api.workos.com". - request_timeout: HTTP request timeout in seconds. Falls back to WORKOS_REQUEST_TIMEOUT or 25. + request_timeout: HTTP request timeout in seconds. Falls back to WORKOS_REQUEST_TIMEOUT or 30. jwt_leeway: JWT clock skew leeway in seconds. max_retries: Maximum number of retries for failed requests. Defaults to 3. diff --git a/src/workos/audit_logs/_resource.py b/src/workos/audit_logs/_resource.py index b45a046c..5cd04bfa 100644 --- a/src/workos/audit_logs/_resource.py +++ b/src/workos/audit_logs/_resource.py @@ -219,7 +219,7 @@ def create_events( request_options=request_options, ) - def exports( + def create_export( self, *, organization_id: str, @@ -278,7 +278,11 @@ def exports( request_options=request_options, ) - def export( + def exports(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `create_export`.""" + return self.create_export(*args, **kwargs) + + def get_export( self, audit_log_export_id: str, *, @@ -308,6 +312,10 @@ def export( request_options=request_options, ) + def export(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `get_export`.""" + return self.get_export(*args, **kwargs) + class AsyncAuditLogs: """Audit Logs API resources (async).""" @@ -507,7 +515,7 @@ async def create_events( request_options=request_options, ) - async def exports( + async def create_export( self, *, organization_id: str, @@ -566,7 +574,11 @@ async def exports( request_options=request_options, ) - async def export( + async def exports(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `create_export`.""" + return await self.create_export(*args, **kwargs) + + async def get_export( self, audit_log_export_id: str, *, @@ -595,3 +607,7 @@ async def export( model=AuditLogExportJson, request_options=request_options, ) + + async def export(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `get_export`.""" + return await self.get_export(*args, **kwargs) diff --git a/src/workos/authorization/_resource.py b/src/workos/authorization/_resource.py index aab88013..40f18fbc 100644 --- a/src/workos/authorization/_resource.py +++ b/src/workos/authorization/_resource.py @@ -534,7 +534,7 @@ def delete_roles(self, *args: Any, **kwargs: Any) -> Any: """Compatibility alias for `delete_organization_role`.""" return self.delete_organization_role(*args, **kwargs) - def add_permission_permissions_organizations_roles( + def add_organization_role_permission( self, organization_id: str, slug: str, @@ -575,7 +575,13 @@ def add_permission_permissions_organizations_roles( request_options=request_options, ) - def set_permissions_permissions_organizations_roles( + def add_permission_permissions_organizations_roles( + self, *args: Any, **kwargs: Any + ) -> Any: + """Compatibility alias for `add_organization_role_permission`.""" + return self.add_organization_role_permission(*args, **kwargs) + + def set_organization_role_permissions( self, organization_id: str, slug: str, @@ -615,7 +621,13 @@ def set_permissions_permissions_organizations_roles( request_options=request_options, ) - def remove_permission( + def set_permissions_permissions_organizations_roles( + self, *args: Any, **kwargs: Any + ) -> Any: + """Compatibility alias for `set_organization_role_permissions`.""" + return self.set_organization_role_permissions(*args, **kwargs) + + def remove_organization_role_permission( self, organization_id: str, slug: str, @@ -646,6 +658,10 @@ def remove_permission( request_options=request_options, ) + def remove_permission(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `remove_organization_role_permission`.""" + return self.remove_organization_role_permission(*args, **kwargs) + def get_by_external_id( self, organization_id: str, @@ -1317,7 +1333,7 @@ def update_roles( request_options=request_options, ) - def add_permission_permissions_roles( + def add_environment_role_permission( self, slug: str, *, @@ -1356,7 +1372,11 @@ def add_permission_permissions_roles( request_options=request_options, ) - def set_permissions_permissions_roles( + def add_permission_permissions_roles(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `add_environment_role_permission`.""" + return self.add_environment_role_permission(*args, **kwargs) + + def set_environment_role_permissions( self, slug: str, *, @@ -1395,6 +1415,10 @@ def set_permissions_permissions_roles( request_options=request_options, ) + def set_permissions_permissions_roles(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `set_environment_role_permissions`.""" + return self.set_environment_role_permissions(*args, **kwargs) + class AsyncAuthorization: """Authorization API resources (async).""" @@ -1909,7 +1933,7 @@ async def delete_roles(self, *args: Any, **kwargs: Any) -> Any: """Compatibility alias for `delete_organization_role`.""" return await self.delete_organization_role(*args, **kwargs) - async def add_permission_permissions_organizations_roles( + async def add_organization_role_permission( self, organization_id: str, slug: str, @@ -1950,7 +1974,13 @@ async def add_permission_permissions_organizations_roles( request_options=request_options, ) - async def set_permissions_permissions_organizations_roles( + async def add_permission_permissions_organizations_roles( + self, *args: Any, **kwargs: Any + ) -> Any: + """Compatibility alias for `add_organization_role_permission`.""" + return await self.add_organization_role_permission(*args, **kwargs) + + async def set_organization_role_permissions( self, organization_id: str, slug: str, @@ -1990,7 +2020,13 @@ async def set_permissions_permissions_organizations_roles( request_options=request_options, ) - async def remove_permission( + async def set_permissions_permissions_organizations_roles( + self, *args: Any, **kwargs: Any + ) -> Any: + """Compatibility alias for `set_organization_role_permissions`.""" + return await self.set_organization_role_permissions(*args, **kwargs) + + async def remove_organization_role_permission( self, organization_id: str, slug: str, @@ -2021,6 +2057,10 @@ async def remove_permission( request_options=request_options, ) + async def remove_permission(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `remove_organization_role_permission`.""" + return await self.remove_organization_role_permission(*args, **kwargs) + async def get_by_external_id( self, organization_id: str, @@ -2692,7 +2732,7 @@ async def update_roles( request_options=request_options, ) - async def add_permission_permissions_roles( + async def add_environment_role_permission( self, slug: str, *, @@ -2731,7 +2771,11 @@ async def add_permission_permissions_roles( request_options=request_options, ) - async def set_permissions_permissions_roles( + async def add_permission_permissions_roles(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `add_environment_role_permission`.""" + return await self.add_environment_role_permission(*args, **kwargs) + + async def set_environment_role_permissions( self, slug: str, *, @@ -2769,3 +2813,7 @@ async def set_permissions_permissions_roles( model=Role, request_options=request_options, ) + + async def set_permissions_permissions_roles(self, *args: Any, **kwargs: Any) -> Any: + """Compatibility alias for `set_environment_role_permissions`.""" + return await self.set_environment_role_permissions(*args, **kwargs) diff --git a/src/workos/events/models/event.py b/src/workos/events/models/event.py index bb5accd2..477936a7 100644 --- a/src/workos/events/models/event.py +++ b/src/workos/events/models/event.py @@ -3,21 +3,34 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, Dict +from typing import Any, Dict, Optional from workos._errors import BaseRequestException +# @oagen-ignore-start @dataclass(slots=True) class Event: - """An event emitted by WorkOS.""" + """An event emitted by WorkOS. - pass + The ``event`` field is the discriminator (e.g. ``"dsync.activated"``). + ``data`` contains the event-specific payload as a dictionary. + """ + + id: str + event: str + data: Dict[str, Any] + created_at: Optional[str] = None @classmethod def from_dict(cls, data: Dict[str, Any]) -> "Event": """Deserialize from a dictionary.""" try: - return cls() + return cls( + id=data["id"], + event=data["event"], + data=data.get("data", {}), + created_at=data.get("created_at"), + ) except (KeyError, ValueError) as e: raise BaseRequestException( f"Unexpected API response while parsing Event: {e!s}" @@ -25,5 +38,14 @@ def from_dict(cls, data: Dict[str, Any]) -> "Event": def to_dict(self) -> Dict[str, Any]: """Serialize to a dictionary.""" - result: Dict[str, Any] = {} + result: Dict[str, Any] = { + "id": self.id, + "event": self.event, + "data": self.data, + } + if self.created_at is not None: + result["created_at"] = self.created_at return result + + +# @oagen-ignore-end diff --git a/tests/fixtures/event.json b/tests/fixtures/event.json index 0967ef42..6545d9fc 100644 --- a/tests/fixtures/event.json +++ b/tests/fixtures/event.json @@ -1 +1,9 @@ -{} +{ + "id": "event_01FGCPNV312FHFRCX0BYQVBMEQ", + "event": "dsync.activated", + "data": { + "id": "directory_01FGCPNV312FHFRCX0BYQVBMEQ", + "name": "Foo Corp's Directory" + }, + "created_at": "2021-10-28T15:13:51.486Z" +} diff --git a/tests/fixtures/list_event.json b/tests/fixtures/list_event.json index 33babb6e..0415cced 100644 --- a/tests/fixtures/list_event.json +++ b/tests/fixtures/list_event.json @@ -1,6 +1,14 @@ { "data": [ - {} + { + "id": "event_01FGCPNV312FHFRCX0BYQVBMEQ", + "event": "dsync.activated", + "data": { + "id": "directory_01FGCPNV312FHFRCX0BYQVBMEQ", + "name": "Foo Corp's Directory" + }, + "created_at": "2021-10-28T15:13:51.486Z" + } ], "list_metadata": { "before": null, diff --git a/tests/test_audit_logs.py b/tests/test_audit_logs.py index 179f3216..1fb01ca5 100644 --- a/tests/test_audit_logs.py +++ b/tests/test_audit_logs.py @@ -111,11 +111,11 @@ def test_create_events(self, workos, httpx_mock): assert body["organization_id"] == "test_organization_id" assert "event" in body - def test_exports(self, workos, httpx_mock): + def test_create_export(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("audit_log_export_json.json"), ) - result = workos.audit_logs.exports( + result = workos.audit_logs.create_export( organization_id="test_organization_id", range_start="test_range_start", range_end="test_range_end", @@ -131,11 +131,11 @@ def test_exports(self, workos, httpx_mock): assert body["range_start"] == "test_range_start" assert body["range_end"] == "test_range_end" - def test_export(self, workos, httpx_mock): + def test_get_export(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("audit_log_export_json.json"), ) - result = workos.audit_logs.export("test_auditLogExportId") + result = workos.audit_logs.get_export("test_auditLogExportId") assert isinstance(result, AuditLogExportJson) assert result.object == "audit_log_export" assert result.id == "audit_log_export_01GBZK5MP7TD1YCFQHFR22180V" @@ -264,9 +264,9 @@ async def test_create_events(self, async_workos, httpx_mock): assert request.method == "POST" assert request.url.path.endswith("/audit_logs/events") - async def test_exports(self, async_workos, httpx_mock): + async def test_create_export(self, async_workos, httpx_mock): httpx_mock.add_response(json=load_fixture("audit_log_export_json.json")) - result = await async_workos.audit_logs.exports( + result = await async_workos.audit_logs.create_export( organization_id="test_organization_id", range_start="test_range_start", range_end="test_range_end", @@ -278,9 +278,9 @@ async def test_exports(self, async_workos, httpx_mock): assert request.method == "POST" assert request.url.path.endswith("/audit_logs/exports") - async def test_export(self, async_workos, httpx_mock): + async def test_get_export(self, async_workos, httpx_mock): httpx_mock.add_response(json=load_fixture("audit_log_export_json.json")) - result = await async_workos.audit_logs.export("test_auditLogExportId") + result = await async_workos.audit_logs.get_export("test_auditLogExportId") assert isinstance(result, AuditLogExportJson) assert result.object == "audit_log_export" assert result.id == "audit_log_export_01GBZK5MP7TD1YCFQHFR22180V" diff --git a/tests/test_authorization.py b/tests/test_authorization.py index 97f0f858..f8ab1ce8 100644 --- a/tests/test_authorization.py +++ b/tests/test_authorization.py @@ -245,11 +245,11 @@ def test_delete_organization_role(self, workos, httpx_mock): "/authorization/organizations/test_organizationId/roles/test_slug" ) - def test_add_permission_permissions_organizations_roles(self, workos, httpx_mock): + def test_add_organization_role_permission(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("role.json"), ) - result = workos.authorization.add_permission_permissions_organizations_roles( + result = workos.authorization.add_organization_role_permission( "test_organizationId", "test_slug", body_slug="test_slug" ) assert isinstance(result, Role) @@ -263,11 +263,11 @@ def test_add_permission_permissions_organizations_roles(self, workos, httpx_mock body = json.loads(request.content) assert body["slug"] == "test_slug" - def test_set_permissions_permissions_organizations_roles(self, workos, httpx_mock): + def test_set_organization_role_permissions(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("role.json"), ) - result = workos.authorization.set_permissions_permissions_organizations_roles( + result = workos.authorization.set_organization_role_permissions( "test_organizationId", "test_slug", permissions=[] ) assert isinstance(result, Role) @@ -281,9 +281,9 @@ def test_set_permissions_permissions_organizations_roles(self, workos, httpx_moc body = json.loads(request.content) assert "permissions" in body - def test_remove_permission(self, workos, httpx_mock): + def test_remove_organization_role_permission(self, workos, httpx_mock): httpx_mock.add_response(status_code=204) - result = workos.authorization.remove_permission( + result = workos.authorization.remove_organization_role_permission( "test_organizationId", "test_slug", "test_permissionSlug" ) assert result is None @@ -599,11 +599,11 @@ def test_update_roles(self, workos, httpx_mock): assert request.method == "PATCH" assert request.url.path.endswith("/authorization/roles/test_slug") - def test_add_permission_permissions_roles(self, workos, httpx_mock): + def test_add_environment_role_permission(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("role.json"), ) - result = workos.authorization.add_permission_permissions_roles( + result = workos.authorization.add_environment_role_permission( "test_slug", body_slug="test_slug" ) assert isinstance(result, Role) @@ -615,11 +615,11 @@ def test_add_permission_permissions_roles(self, workos, httpx_mock): body = json.loads(request.content) assert body["slug"] == "test_slug" - def test_set_permissions_permissions_roles(self, workos, httpx_mock): + def test_set_environment_role_permissions(self, workos, httpx_mock): httpx_mock.add_response( json=load_fixture("role.json"), ) - result = workos.authorization.set_permissions_permissions_roles( + result = workos.authorization.set_environment_role_permissions( "test_slug", permissions=[] ) assert isinstance(result, Role) @@ -887,11 +887,9 @@ async def test_delete_organization_role(self, async_workos, httpx_mock): "/authorization/organizations/test_organizationId/roles/test_slug" ) - async def test_add_permission_permissions_organizations_roles( - self, async_workos, httpx_mock - ): + async def test_add_organization_role_permission(self, async_workos, httpx_mock): httpx_mock.add_response(json=load_fixture("role.json")) - result = await async_workos.authorization.add_permission_permissions_organizations_roles( + result = await async_workos.authorization.add_organization_role_permission( "test_organizationId", "test_slug", body_slug="test_slug" ) assert isinstance(result, Role) @@ -903,11 +901,9 @@ async def test_add_permission_permissions_organizations_roles( "/authorization/organizations/test_organizationId/roles/test_slug/permissions" ) - async def test_set_permissions_permissions_organizations_roles( - self, async_workos, httpx_mock - ): + async def test_set_organization_role_permissions(self, async_workos, httpx_mock): httpx_mock.add_response(json=load_fixture("role.json")) - result = await async_workos.authorization.set_permissions_permissions_organizations_roles( + result = await async_workos.authorization.set_organization_role_permissions( "test_organizationId", "test_slug", permissions=[] ) assert isinstance(result, Role) @@ -919,9 +915,9 @@ async def test_set_permissions_permissions_organizations_roles( "/authorization/organizations/test_organizationId/roles/test_slug/permissions" ) - async def test_remove_permission(self, async_workos, httpx_mock): + async def test_remove_organization_role_permission(self, async_workos, httpx_mock): httpx_mock.add_response(status_code=204) - result = await async_workos.authorization.remove_permission( + result = await async_workos.authorization.remove_organization_role_permission( "test_organizationId", "test_slug", "test_permissionSlug" ) assert result is None @@ -1223,9 +1219,9 @@ async def test_update_roles(self, async_workos, httpx_mock): assert request.method == "PATCH" assert request.url.path.endswith("/authorization/roles/test_slug") - async def test_add_permission_permissions_roles(self, async_workos, httpx_mock): + async def test_add_environment_role_permission(self, async_workos, httpx_mock): httpx_mock.add_response(json=load_fixture("role.json")) - result = await async_workos.authorization.add_permission_permissions_roles( + result = await async_workos.authorization.add_environment_role_permission( "test_slug", body_slug="test_slug" ) assert isinstance(result, Role) @@ -1235,9 +1231,9 @@ async def test_add_permission_permissions_roles(self, async_workos, httpx_mock): assert request.method == "POST" assert request.url.path.endswith("/authorization/roles/test_slug/permissions") - async def test_set_permissions_permissions_roles(self, async_workos, httpx_mock): + async def test_set_environment_role_permissions(self, async_workos, httpx_mock): httpx_mock.add_response(json=load_fixture("role.json")) - result = await async_workos.authorization.set_permissions_permissions_roles( + result = await async_workos.authorization.set_environment_role_permissions( "test_slug", permissions=[] ) assert isinstance(result, Role) diff --git a/tests/test_models_round_trip.py b/tests/test_models_round_trip.py index 1f87de50..f924a7f9 100644 --- a/tests/test_models_round_trip.py +++ b/tests/test_models_round_trip.py @@ -1719,7 +1719,7 @@ def test_event_round_trip(self): assert restored.to_dict() == serialized def test_event_minimal_payload(self): - data = {} + data = {"id": "event_123", "event": "dsync.activated", "data": {}} instance = Event.from_dict(data) assert instance.to_dict() is not None From 58a70196c4f4ab1db533b5c8b1ec2681bc4ed1e3 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 31 Mar 2026 17:49:58 -0400 Subject: [PATCH 21/26] chore(python): regenerate SDK with empty-model pass fix Regenerated from updated emitter that no longer emits unnecessary `pass` in models with zero fields, fixing ruff format CI failure. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../models/create_application_secret.py | 2 -- src/workos/events/models/event.py | 23 ++++++++++++++++++- .../data_integration_access_token_response.py | 2 -- tests/fixtures/event.json | 10 +------- tests/fixtures/list_event.json | 10 +------- tests/test_models_round_trip.py | 2 +- 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/workos/application_client_secrets/models/create_application_secret.py b/src/workos/application_client_secrets/models/create_application_secret.py index 31f51833..a3de9b6e 100644 --- a/src/workos/application_client_secrets/models/create_application_secret.py +++ b/src/workos/application_client_secrets/models/create_application_secret.py @@ -11,8 +11,6 @@ class CreateApplicationSecret: """Create Application Secret model.""" - pass - @classmethod def from_dict(cls, data: Dict[str, Any]) -> "CreateApplicationSecret": """Deserialize from a dictionary.""" diff --git a/src/workos/events/models/event.py b/src/workos/events/models/event.py index 477936a7..ff2b628e 100644 --- a/src/workos/events/models/event.py +++ b/src/workos/events/models/event.py @@ -3,8 +3,29 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, Dict, Optional +from typing import Any, Dict from workos._errors import BaseRequestException +from typing import Optional + + +@dataclass(slots=True) +class Event: + """An event emitted by WorkOS.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Event": + """Deserialize from a dictionary.""" + try: + return cls() + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing Event: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + return result # @oagen-ignore-start diff --git a/src/workos/pipes/models/data_integration_access_token_response.py b/src/workos/pipes/models/data_integration_access_token_response.py index 95b481ee..7153b99b 100644 --- a/src/workos/pipes/models/data_integration_access_token_response.py +++ b/src/workos/pipes/models/data_integration_access_token_response.py @@ -11,8 +11,6 @@ class DataIntegrationAccessTokenResponse: """Data Integration Access Token Response model.""" - pass - @classmethod def from_dict(cls, data: Dict[str, Any]) -> "DataIntegrationAccessTokenResponse": """Deserialize from a dictionary.""" diff --git a/tests/fixtures/event.json b/tests/fixtures/event.json index 6545d9fc..0967ef42 100644 --- a/tests/fixtures/event.json +++ b/tests/fixtures/event.json @@ -1,9 +1 @@ -{ - "id": "event_01FGCPNV312FHFRCX0BYQVBMEQ", - "event": "dsync.activated", - "data": { - "id": "directory_01FGCPNV312FHFRCX0BYQVBMEQ", - "name": "Foo Corp's Directory" - }, - "created_at": "2021-10-28T15:13:51.486Z" -} +{} diff --git a/tests/fixtures/list_event.json b/tests/fixtures/list_event.json index 0415cced..33babb6e 100644 --- a/tests/fixtures/list_event.json +++ b/tests/fixtures/list_event.json @@ -1,14 +1,6 @@ { "data": [ - { - "id": "event_01FGCPNV312FHFRCX0BYQVBMEQ", - "event": "dsync.activated", - "data": { - "id": "directory_01FGCPNV312FHFRCX0BYQVBMEQ", - "name": "Foo Corp's Directory" - }, - "created_at": "2021-10-28T15:13:51.486Z" - } + {} ], "list_metadata": { "before": null, diff --git a/tests/test_models_round_trip.py b/tests/test_models_round_trip.py index f924a7f9..1f87de50 100644 --- a/tests/test_models_round_trip.py +++ b/tests/test_models_round_trip.py @@ -1719,7 +1719,7 @@ def test_event_round_trip(self): assert restored.to_dict() == serialized def test_event_minimal_payload(self): - data = {"id": "event_123", "event": "dsync.activated", "data": {}} + data = {} instance = Event.from_dict(data) assert instance.to_dict() is not None From 4138e96b00502622323118cb435a8192a65bd55d Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 31 Mar 2026 18:00:40 -0400 Subject: [PATCH 22/26] chore(python): regenerate SDK with ignore-block class replacement fix The generated Event stub is now replaced by the @oagen-ignore block version, eliminating the F811 duplicate class definition lint error. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/workos/events/models/event.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/workos/events/models/event.py b/src/workos/events/models/event.py index ff2b628e..6aa7bc6a 100644 --- a/src/workos/events/models/event.py +++ b/src/workos/events/models/event.py @@ -8,26 +8,6 @@ from typing import Optional -@dataclass(slots=True) -class Event: - """An event emitted by WorkOS.""" - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "Event": - """Deserialize from a dictionary.""" - try: - return cls() - except (KeyError, ValueError) as e: - raise BaseRequestException( - f"Unexpected API response while parsing Event: {e!s}" - ) from e - - def to_dict(self) -> Dict[str, Any]: - """Serialize to a dictionary.""" - result: Dict[str, Any] = {} - return result - - # @oagen-ignore-start @dataclass(slots=True) class Event: From f62010c49967d139c5ab187df62513632e68032d Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 31 Mar 2026 22:49:56 -0400 Subject: [PATCH 23/26] chore(python): regenerate SDK with ignore-block and fixture fixes Regenerated from updated oagen core (top-level @oagen-ignore replacement) and emitters (skip fixtures/tests for empty discriminated union models). Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/test_events.py | 17 ----------------- tests/test_models_round_trip.py | 32 +------------------------------- 2 files changed, 1 insertion(+), 48 deletions(-) diff --git a/tests/test_events.py b/tests/test_events.py index 278d5b16..bd464407 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -3,7 +3,6 @@ import pytest from workos import WorkOS, AsyncWorkOS -from tests.generated_helpers import load_fixture from workos.events.models import EventsOrder from workos._pagination import AsyncPage, SyncPage @@ -17,18 +16,9 @@ class TestEvents: def test_list(self, workos, httpx_mock): - httpx_mock.add_response( - json=load_fixture("list_event.json"), - ) - page = workos.events.list() - assert isinstance(page, SyncPage) - assert isinstance(page.data, list) - - def test_list_empty_page(self, workos, httpx_mock): httpx_mock.add_response(json={"data": [], "list_metadata": {}}) page = workos.events.list() assert isinstance(page, SyncPage) - assert page.data == [] def test_list_encodes_query_params(self, workos, httpx_mock): httpx_mock.add_response(json={"data": [], "list_metadata": {}}) @@ -93,16 +83,9 @@ def test_list_server_error(self, httpx_mock): @pytest.mark.asyncio class TestAsyncEvents: async def test_list(self, async_workos, httpx_mock): - httpx_mock.add_response(json=load_fixture("list_event.json")) - page = await async_workos.events.list() - assert isinstance(page, AsyncPage) - assert isinstance(page.data, list) - - async def test_list_empty_page(self, async_workos, httpx_mock): httpx_mock.add_response(json={"data": [], "list_metadata": {}}) page = await async_workos.events.list() assert isinstance(page, AsyncPage) - assert page.data == [] async def test_list_encodes_query_params(self, async_workos, httpx_mock): httpx_mock.add_response(json={"data": [], "list_metadata": {}}) diff --git a/tests/test_models_round_trip.py b/tests/test_models_round_trip.py index 1f87de50..e9907082 100644 --- a/tests/test_models_round_trip.py +++ b/tests/test_models_round_trip.py @@ -52,7 +52,6 @@ DirectoryUserWithGroups, DirectoryUserWithGroupsEmail, ) -from workos.events.models import Event from workos.feature_flags.models import FeatureFlag, FeatureFlagOwner, Flag, FlagOwner from workos.multi_factor_auth.models import ( AuthenticationFactor, @@ -79,10 +78,7 @@ ) from workos.organizations.api_keys.models import ApiKeyWithValue, ApiKeyWithValueOwner from workos.permissions.models import AuthorizationPermission, Permission -from workos.pipes.models import ( - DataIntegrationAccessTokenResponse, - DataIntegrationAuthorizeUrlResponse, -) +from workos.pipes.models import DataIntegrationAuthorizeUrlResponse from workos.radar.models import ( RadarListEntryAlreadyPresentResponse, RadarStandaloneResponse, @@ -1710,19 +1706,6 @@ def test_user_preserves_nullable_fields(self): assert serialized["last_sign_in_at"] is None assert serialized["locale"] is None - def test_event_round_trip(self): - data = load_fixture("event.json") - instance = Event.from_dict(data) - serialized = instance.to_dict() - assert serialized == data - restored = Event.from_dict(serialized) - assert restored.to_dict() == serialized - - def test_event_minimal_payload(self): - data = {} - instance = Event.from_dict(data) - assert instance.to_dict() is not None - def test_jwt_template_response_round_trip(self): data = load_fixture("jwt_template_response.json") instance = JWTTemplateResponse.from_dict(data) @@ -2077,19 +2060,6 @@ def test_data_integration_authorize_url_response_minimal_payload(self): serialized = instance.to_dict() assert serialized["url"] == data["url"] - def test_data_integration_access_token_response_round_trip(self): - data = load_fixture("data_integration_access_token_response.json") - instance = DataIntegrationAccessTokenResponse.from_dict(data) - serialized = instance.to_dict() - assert serialized == data - restored = DataIntegrationAccessTokenResponse.from_dict(serialized) - assert restored.to_dict() == serialized - - def test_data_integration_access_token_response_minimal_payload(self): - data = {} - instance = DataIntegrationAccessTokenResponse.from_dict(data) - assert instance.to_dict() is not None - def test_connected_account_round_trip(self): data = load_fixture("connected_account.json") instance = ConnectedAccount.from_dict(data) From 901c70aedb7b3a07ba0336ac338fcd78fe74365e Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Tue, 31 Mar 2026 23:02:21 -0400 Subject: [PATCH 24/26] Regenerate client credential handling --- src/workos/_client.py | 36 +++++++++++++++++++++++----------- src/workos/sso/_resource.py | 20 +++++++++++++------ tests/test_generated_client.py | 21 +++++++++++++++++--- 3 files changed, 57 insertions(+), 20 deletions(-) diff --git a/src/workos/_client.py b/src/workos/_client.py index f3f72751..d5838f0c 100644 --- a/src/workos/_client.py +++ b/src/workos/_client.py @@ -17,6 +17,7 @@ from ._errors import ( BaseRequestException, + ConfigurationException, RateLimitExceededException, ServerException, WorkOSConnectionException, @@ -401,16 +402,12 @@ def __init__( max_retries: int = MAX_RETRIES, ) -> None: self._api_key = api_key or os.environ.get("WORKOS_API_KEY") - if not self._api_key: - raise ValueError( - "WorkOS API key must be provided when instantiating the client " - "or via the WORKOS_API_KEY environment variable." - ) self.client_id = client_id or os.environ.get("WORKOS_CLIENT_ID") - if not self.client_id: + if not self._api_key and not self.client_id: raise ValueError( - "WorkOS client ID must be provided when instantiating the client " - "or via the WORKOS_CLIENT_ID environment variable." + "WorkOS requires either an API key or a client ID. " + "Provide api_key / WORKOS_API_KEY for authenticated server-side usage, " + "or client_id / WORKOS_CLIENT_ID for flows that require a client ID." ) resolved_base_url = base_url or os.environ.get( "WORKOS_BASE_URL", "https://api.workos.com" @@ -493,6 +490,22 @@ def _resolve_max_retries(self, request_options: Optional[RequestOptions]) -> int return retries return self._max_retries + def _require_api_key(self) -> str: + if not self._api_key: + raise ConfigurationException( + "This operation requires a WorkOS API key. Provide api_key when instantiating the client " + "or via the WORKOS_API_KEY environment variable." + ) + return self._api_key + + def _require_client_id(self) -> str: + if not self.client_id: + raise ConfigurationException( + "This operation requires a WorkOS client ID. Provide client_id when instantiating the client " + "or via the WORKOS_CLIENT_ID environment variable." + ) + return self.client_id + def _build_headers( self, method: str, @@ -500,10 +513,11 @@ def _build_headers( request_options: Optional[RequestOptions], ) -> Dict[str, str]: headers: Dict[str, str] = { - "Authorization": f"Bearer {self._api_key}", "Content-Type": "application/json", "User-Agent": f"workos-python/{VERSION} python/{platform.python_version()}", } + if self._api_key: + headers["Authorization"] = f"Bearer {self._api_key}" effective_idempotency_key = idempotency_key if effective_idempotency_key is None and request_options: request_option_idempotency_key = request_options.get("idempotency_key") @@ -652,7 +666,7 @@ def __init__( max_retries: Maximum number of retries for failed requests. Defaults to 3. Raises: - ValueError: If api_key is not provided and WORKOS_API_KEY is not set. + ValueError: If neither api_key nor client_id is provided, directly or via environment variables. """ super().__init__( api_key=api_key, @@ -931,7 +945,7 @@ def __init__( max_retries: Maximum number of retries for failed requests. Defaults to 3. Raises: - ValueError: If api_key is not provided and WORKOS_API_KEY is not set. + ValueError: If neither api_key nor client_id is provided, directly or via environment variables. """ super().__init__( api_key=api_key, diff --git a/src/workos/sso/_resource.py b/src/workos/sso/_resource.py index 3cdf83ec..cfc5b503 100644 --- a/src/workos/sso/_resource.py +++ b/src/workos/sso/_resource.py @@ -91,7 +91,9 @@ def authorize( }.items() if v is not None } - params["client_id"] = params.get("client_id") or self._client.client_id + params["client_id"] = ( + params.get("client_id") or self._client._require_client_id() + ) return self._client.build_url("sso/authorize", params) def logout( @@ -238,8 +240,10 @@ def token( "code": code, "grant_type": grant_type, } - body["client_id"] = body.get("client_id") or self._client.client_id - body["client_secret"] = body.get("client_secret") or self._client._api_key + body["client_id"] = body.get("client_id") or self._client._require_client_id() + body["client_secret"] = ( + body.get("client_secret") or self._client._require_api_key() + ) return self._client.request( method="post", path="sso/token", @@ -407,7 +411,9 @@ async def authorize( }.items() if v is not None } - params["client_id"] = params.get("client_id") or self._client.client_id + params["client_id"] = ( + params.get("client_id") or self._client._require_client_id() + ) return self._client.build_url("sso/authorize", params) async def logout( @@ -554,8 +560,10 @@ async def token( "code": code, "grant_type": grant_type, } - body["client_id"] = body.get("client_id") or self._client.client_id - body["client_secret"] = body.get("client_secret") or self._client._api_key + body["client_id"] = body.get("client_id") or self._client._require_client_id() + body["client_secret"] = ( + body.get("client_secret") or self._client._require_api_key() + ) return await self._client.request( method="post", path="sso/token", diff --git a/tests/test_generated_client.py b/tests/test_generated_client.py index 4a574300..0b379b44 100644 --- a/tests/test_generated_client.py +++ b/tests/test_generated_client.py @@ -20,17 +20,24 @@ class TestWorkOSClient: - def test_missing_api_key_raises(self): + def test_missing_credentials_raise(self): with pytest.raises(ValueError): - WorkOS(client_id="client_test") + WorkOS() def test_context_manager(self): with WorkOS(api_key="sk_test_123", client_id="client_test") as client: assert client._api_key == "sk_test_123" + def test_api_key_only_initializes(self): + client = WorkOS(api_key="sk_test_123") + assert client._api_key == "sk_test_123" + assert client.client_id is None + client.close() + def test_client_id_from_constructor(self): - client = WorkOS(api_key="sk_test_123", client_id="client_test_456") + client = WorkOS(client_id="client_test_456") assert client.client_id == "client_test_456" + assert client._api_key is None client.close() def test_raises_400(self, httpx_mock): @@ -129,6 +136,14 @@ def test_no_idempotency_key_on_get(self, httpx_mock): assert "Idempotency-Key" not in request.headers client.close() + def test_no_authorization_header_without_api_key(self, httpx_mock): + httpx_mock.add_response(json={}) + client = WorkOS(client_id="client_test") + client.request("GET", "test") + request = httpx_mock.get_request() + assert "Authorization" not in request.headers + client.close() + def test_empty_body_sends_json(self, httpx_mock): httpx_mock.add_response(json={}) client = WorkOS(api_key="sk_test_123", client_id="client_test") From f1f0d8f9e4733e0aa654937b304d7349d2be5916 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Wed, 1 Apr 2026 12:52:28 -0400 Subject: [PATCH 25/26] initial migration guide --- docs/V6_MIGRATION_GUIDE.md | 479 +++++++++++++++++++++++++++++++++++++ 1 file changed, 479 insertions(+) create mode 100644 docs/V6_MIGRATION_GUIDE.md diff --git a/docs/V6_MIGRATION_GUIDE.md b/docs/V6_MIGRATION_GUIDE.md new file mode 100644 index 00000000..1254e83a --- /dev/null +++ b/docs/V6_MIGRATION_GUIDE.md @@ -0,0 +1,479 @@ +# WorkOS Python SDK v6 Migration Guide + +This guide covers the main breaking changes from v5 to v6 of the WorkOS Python SDK. + +v6 moves the SDK onto the generated client surface, retires most of the old handwritten module layout, and standardizes models, pagination, errors, and request handling. Most upgrades are mechanical, but there are a few places where behavior changed enough that it is worth reviewing your integration carefully. + +Read this as the migration guide for the generated-surface major release, not as a statement about the package version currently checked into this branch. + +## Table of Contents + +- [WorkOS Python SDK v6 Migration Guide](#workos-python-sdk-v6-migration-guide) + - [Table of Contents](#table-of-contents) + - [Quick Start](#quick-start) + - [Breaking Changes](#breaking-changes) + - [Python 3.10+ is now required](#python-310-is-now-required) + - [`client_id` is only required for flows that use it](#client_id-is-only-required-for-flows-that-use-it) + - [Legacy client modules were removed](#legacy-client-modules-were-removed) + - [SDK exceptions now come from `workos`, not `workos.exceptions`](#sdk-exceptions-now-come-from-workos-not-workosexceptions) + - [Models no longer live under `workos.types.*`](#models-no-longer-live-under-workostypes) + - [SDK models are no longer Pydantic models](#sdk-models-are-no-longer-pydantic-models) + - [`client.connect` was split into explicit resources](#clientconnect-was-split-into-explicit-resources) + - [`client.directory_sync` was split into directories, groups, and users](#clientdirectory_sync-was-split-into-directories-groups-and-users) + - [`client.portal` became `client.admin_portal`](#clientportal-became-clientadmin_portal) + - [Old `user_management` convenience helpers moved onto sub-resources](#old-user_management-convenience-helpers-moved-onto-sub-resources) + - [AuthKit authentication helpers now use explicit request bodies](#authkit-authentication-helpers-now-use-explicit-request-bodies) + - [Some `user_management` methods moved and were renamed](#some-user_management-methods-moved-and-were-renamed) + - [Permission CRUD moved from `authorization` to `permissions`](#permission-crud-moved-from-authorization-to-permissions) + - [The deprecated `client.fga` module was removed](#the-deprecated-clientfga-module-was-removed) + - [Paginated responses are now `SyncPage` / `AsyncPage`](#paginated-responses-are-now-syncpage--asyncpage) + - [Exception objects expose a more normalized error surface](#exception-objects-expose-a-more-normalized-error-surface) + - [The SDK now retries certain failures by default](#the-sdk-now-retries-certain-failures-by-default) + - [Suggested Upgrade Order](#suggested-upgrade-order) + - [Final Checklist](#final-checklist) + +## Quick Start + +1. Upgrade to Python 3.10 or newer. +2. Update to the v6 release. +3. Replace legacy imports from `workos.client`, `workos.async_client`, `workos.exceptions`, and `workos.types.*`. +4. Update any uses of `connect`, `portal`, `directory_sync`, `fga`, `authorization` permission helpers, and old `user_management` convenience helpers. +5. Run your tests and look specifically for import errors, missing methods, and model serialization issues. + +## Breaking Changes + +### Python 3.10+ is now required + +**5.x (old):** + +```toml +[project] +requires-python = ">=3.8" +``` + +**6.x (new):** + +```toml +[project] +requires-python = ">=3.10" +``` + +**Affected users:** Anyone still running Python 3.8 or 3.9 in production, CI, or local development +**Migration:** Upgrade your runtime and CI image to Python 3.10+ before upgrading the SDK + +### `client_id` is only required for flows that use it + +**5.x (old):** + +```python +from workos import WorkOSClient + +client = WorkOSClient(api_key="sk_test_123") +``` + +**6.x (new):** + +```python +from workos import WorkOSClient + +server_client = WorkOSClient(api_key="sk_test_123") +public_client = WorkOSClient(client_id="client_123") +``` + +**Affected users:** Anyone using SSO, AuthKit, session helpers, or other flows that depend on a client ID fallback from the SDK client +**Migration:** Keep using `api_key`-only construction for server-side API usage. For flows that need a client ID, pass it explicitly to the method or configure it via `WorkOSClient(client_id=...)` or `WORKOS_CLIENT_ID`. The generated client now requires at least one credential at construction time, not both. + +### Legacy client modules were removed + +**5.x (old):** + +```python +from workos.client import SyncClient +from workos.async_client import AsyncClient + +client = SyncClient(api_key="sk_test_123", client_id="client_123") +``` + +**6.x (new):** + +```python +from workos import WorkOSClient, AsyncWorkOSClient + +client = WorkOSClient(api_key="sk_test_123", client_id="client_123") +``` + +**Affected users:** Anyone importing `SyncClient`, `AsyncClient`, or other client classes from legacy client modules +**Migration:** Import clients from the top-level `workos` package instead of `workos.client` or `workos.async_client` + +### SDK exceptions now come from `workos`, not `workos.exceptions` + +**5.x (old):** + +```python +from workos.exceptions import BaseRequestException + +try: + client.organizations.get_organization("org_123") +except BaseRequestException as exc: + print(exc.request_id) +``` + +**6.x (new):** + +```python +from workos import BaseRequestException + +try: + client.organizations.get("org_123") +except BaseRequestException as exc: + print(exc.request_id) +``` + +**Affected users:** Anyone importing exception classes from `workos.exceptions` +**Migration:** Import exception classes directly from `workos` + +### Models no longer live under `workos.types.*` + +**5.x (old):** + +```python +from workos.types.organizations import Organization +from workos.types.connect import ConnectApplication +``` + +**6.x (new):** + +```python +from workos.organizations.models import Organization +from workos.applications.models import ConnectApplication +``` + +**Affected users:** Anyone importing SDK models from `workos.types.*` +**Migration:** Import models from the generated `workos..models` packages + +### SDK models are no longer Pydantic models + +**5.x (old):** + +```python +from workos.types.organizations import Organization + +organization = Organization.model_validate(payload) +body = organization.model_dump() +``` + +**6.x (new):** + +```python +from workos.organizations.models import Organization + +organization = Organization.from_dict(payload) +body = organization.to_dict() +``` + +**Affected users:** Anyone calling `model_validate()`, `model_dump()`, or relying on other Pydantic model behavior +**Migration:** Switch to `from_dict()` / `to_dict()` and stop treating SDK models as Pydantic objects + +### `client.connect` was split into explicit resources + +**5.x (old):** + +```python +applications = client.connect.list_applications() +secret = client.connect.create_client_secret("app_123") +``` + +**6.x (new):** + +```python +applications = client.applications.list() +secret = client.application_client_secrets.create("app_123") +``` + +**Affected users:** Anyone using the old Connect namespace for application or client-secret management +**Migration:** Use `client.applications` for Connect apps and `client.application_client_secrets` for client secret operations + +### `client.directory_sync` was split into directories, groups, and users + +**5.x (old):** + +```python +directories = client.directory_sync.list_directories() +users = client.directory_sync.list_users(directory_id="dir_123") +groups = client.directory_sync.list_groups(user_id="directory_user_123") +``` + +**6.x (new):** + +```python +directories = client.directories.list() +users = client.directory_users.list(directory="dir_123") +groups = client.directory_groups.list(user="directory_user_123") +``` + +**Affected users:** Anyone using `client.directory_sync` +**Migration:** Replace `directory_sync` calls with `directories`, `directory_users`, and `directory_groups` + +### `client.portal` became `client.admin_portal` + +**5.x (old):** + +```python +link = client.portal.generate_link( + organization_id="org_123", + intent="sso", +) +``` + +**6.x (new):** + +```python +link = client.admin_portal.generate_link( + organization="org_123", + intent="sso", +) +``` + +**Affected users:** Anyone generating Admin Portal links through `client.portal` +**Migration:** Move to `client.admin_portal` and rename `organization_id` to `organization` + +### Old `user_management` convenience helpers moved onto sub-resources + +**5.x (old):** + +```python +user = client.user_management.get_user("user_123") + +url = client.user_management.get_authorization_url( + provider="authkit", + redirect_uri="https://example.com/callback", +) +``` + +**6.x (new):** + +```python +user = client.user_management.users.get_user("user_123") + +url = client.user_management.authentication.authorize( + provider="authkit", + redirect_uri="https://example.com/callback", + response_type="code", + client_id=client.client_id, +) +``` + +**Affected users:** Anyone calling helper methods directly on `client.user_management` +**Migration:** Use the generated sub-resources like `client.user_management.users` and `client.user_management.authentication` + +### AuthKit authentication helpers now use explicit request bodies + +**5.x (old):** + +```python +auth = client.user_management.authenticate_with_password( + email="a@b.com", + password="pw", +) +``` + +**6.x (new):** + +```python +from workos.user_management.authentication.models import ( + PasswordSessionAuthenticateRequest, +) + +auth = client.user_management.authentication.authenticate( + body=PasswordSessionAuthenticateRequest( + client_id=client.client_id, + client_secret="sk_test_123", + grant_type="password", + email="a@b.com", + password="pw", + ) +) +``` + +**Affected users:** Anyone using `authenticate_with_password`, `authenticate_with_code`, `authenticate_with_magic_auth`, `authenticate_with_email_verification`, `authenticate_with_totp`, `authenticate_with_organization_selection`, or `authenticate_with_refresh_token` +**Migration:** Replace old helper methods with `client.user_management.authentication.authenticate(body=...)` using the generated request models or a plain dict + +### Some `user_management` methods moved and were renamed + +**5.x (old):** + +```python +password_reset = client.user_management.create_password_reset("a@b.com") +verification = client.user_management.send_verification_email("user_123") +magic_auth = client.user_management.create_magic_auth(email="a@b.com") +``` + +**6.x (new):** + +```python +password_reset = client.user_management.users.create_password_reset_token( + email="a@b.com" +) +verification = client.user_management.users.send_verification_email("user_123") +magic_auth = client.user_management.magic_auth.send_magic_auth_code_and_return( + email="a@b.com" +) +``` + +**Affected users:** Anyone relying on old flat `user_management` method names +**Migration:** Move calls onto the new sub-resources and update method names where the generated surface uses CRUD-style names + +### Permission CRUD moved from `authorization` to `permissions` + +**5.x (old):** + +```python +permission = client.authorization.create_permission( + slug="read:foo", + name="Read Foo", +) +``` + +**6.x (new):** + +```python +permission = client.permissions.create( + slug="read:foo", + name="Read Foo", +) +``` + +**Affected users:** Anyone managing permissions through `client.authorization` +**Migration:** Move permission list/create/get/update/delete calls onto `client.permissions`; keep role and resource operations on `client.authorization` + +### The deprecated `client.fga` module was removed + +**5.x (old):** + +```python +resources = client.fga.list_resources(resource_type="project") +warrants = client.fga.list_warrants(resource_type="project") +check = client.fga.check(...) +``` + +**6.x (new):** + +```python +resources = client.authorization.list_resources(resource_type_slug="project") +assignments = client.authorization.list_role_assignments( + organization_membership_id="om_123" +) +check = client.authorization.check( + organization_membership_id="om_123", + permission_slug="project:read", + resource_id="proj_123", +) +``` + +**Affected users:** Anyone using the deprecated `/fga/v1` SDK surface through `client.fga` +**Migration:** Migrate off `client.fga` onto the current Authorization and Permissions APIs. The mapping is not perfectly 1:1: use `client.authorization` for resource, role, and permission checks; use `client.permissions` for permission CRUD; and update call signatures to the generated parameter names like `resource_id`, `resource_external_id`, and `resource_type_slug` + +### Paginated responses are now `SyncPage` / `AsyncPage` + +**5.x (old):** + +```python +from workos.types.list_resource import WorkOSListResource + +page = client.organizations.list_organizations() +assert isinstance(page, WorkOSListResource) +``` + +**6.x (new):** + +```python +from workos import SyncPage + +page = client.organizations.list() +assert isinstance(page, SyncPage) +``` + +**Affected users:** Anyone importing or type-checking against `WorkOSListResource` +**Migration:** Replace `WorkOSListResource` with `SyncPage` or `AsyncPage`, and prefer `page.before` / `page.after` over reaching into the old list resource internals + +### Exception objects expose a more normalized error surface + +**5.x (old):** + +```python +from workos import BaseRequestException + +try: + client.organizations.get_organization("org_bad") +except BaseRequestException as exc: + print(exc.response.status_code) + print(exc.response_json) +``` + +**6.x (new):** + +```python +from workos import BaseRequestException + +try: + client.organizations.get("org_bad") +except BaseRequestException as exc: + print(exc.status_code) + print(exc.request_id) + print(exc.raw_body) +``` + +**Affected users:** Anyone using try/except with SDK exceptions and inspecting the old raw response shape +**Migration:** Prefer normalized fields like `status_code`, `message`, `request_id`, `raw_body`, `request_url`, and `request_method` + +### The SDK now retries certain failures by default + +**5.x (old):** + +```python +from workos import WorkOSClient + +client = WorkOSClient(api_key="sk_test_123", client_id="client_123") +organization = client.organizations.get_organization("org_123") +``` + +**6.x (new):** + +```python +from workos import WorkOSClient + +client = WorkOSClient( + api_key="sk_test_123", + max_retries=0, +) +organization = client.organizations.get("org_123") +``` + +**Affected users:** Anyone with latency-sensitive code paths, custom retry infrastructure, or assumptions about immediate failure on 429/5xx/network issues +**Migration:** Leave retries enabled if you want the new default behavior, or set `max_retries=0` globally or per request to get closer to 5.x behavior + +## Suggested Upgrade Order + +1. Upgrade Python to 3.10+. +2. Review which flows need `client_id` and either pass it explicitly or configure it on the client / environment. +3. Fix imports from `workos.client`, `workos.async_client`, `workos.exceptions`, and `workos.types.*`. +4. Replace old namespaces: `connect`, `directory_sync`, `portal`, `fga`, and handwritten `user_management` helpers. +5. Move permission CRUD from `client.authorization` to `client.permissions`. +6. Update model serialization code away from Pydantic helpers. +7. Review retry behavior and set `max_retries=0` where you need strict fail-fast semantics. + +## Final Checklist + +- No imports from `workos.client` +- No imports from `workos.async_client` +- No imports from `workos.exceptions` +- No imports from `workos.types.*` +- Client-ID-based flows explicitly pass `client_id` or configure it on the client / environment +- No remaining uses of `client.connect` +- No remaining uses of `client.directory_sync` +- No remaining uses of `client.portal` +- No remaining uses of `client.fga` +- No remaining uses of `client.authorization.create_permission`, `list_permissions`, `get_permission`, `update_permission`, or `delete_permission` +- No remaining uses of removed Pydantic helpers like `model_dump()` or `model_validate()` + +Once those are cleaned up, most integrations should be in good shape for v6. From 2d51bec30b39106bf4678ae8a082feba934ce133 Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Wed, 1 Apr 2026 13:03:00 -0400 Subject: [PATCH 26/26] chore(python): regenerate SDK with exceptions and types backwards-compat Restores `from workos.exceptions import ...` and `from workos.types. import ...` import paths. Updates V6 migration guide to remove these as breaking changes. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/V6_MIGRATION_GUIDE.md | 54 +---- .../models/audit_log_action_json.py | 1 + .../models/audit_log_schema_json.py | 2 +- .../authorization/models/role_assignment.py | 1 + src/workos/authorization/models/slim_role.py | 2 +- .../models/directory_user_with_groups.py | 1 - src/workos/events/_resource.py | 14 +- src/workos/events/models/__init__.py | 2 +- src/workos/events/models/event_schema.py | 59 +++++ src/workos/exceptions.py | 41 ++++ .../models/authentication_challenge.py | 2 +- ...uthentication_challenge_verify_response.py | 1 + .../models/authentication_factor_enrolled.py | 2 +- src/workos/sso/models/profile.py | 2 +- src/workos/sso/models/sso_token_response.py | 1 + src/workos/types/__init__.py | 2 + src/workos/types/admin_portal/__init__.py | 3 + src/workos/types/api_keys/__init__.py | 3 + .../application_client_secrets/__init__.py | 3 + src/workos/types/applications/__init__.py | 3 + src/workos/types/audit_logs/__init__.py | 3 + src/workos/types/authorization/__init__.py | 3 + src/workos/types/connections/__init__.py | 3 + src/workos/types/directories/__init__.py | 3 + src/workos/types/directory_groups/__init__.py | 3 + src/workos/types/directory_users/__init__.py | 3 + src/workos/types/events/__init__.py | 3 + src/workos/types/feature_flags/__init__.py | 4 + .../types/multi_factor_auth/__init__.py | 4 + .../types/organization_domains/__init__.py | 3 + src/workos/types/organizations/__init__.py | 5 + src/workos/types/permissions/__init__.py | 3 + src/workos/types/pipes/__init__.py | 3 + src/workos/types/radar/__init__.py | 3 + src/workos/types/sso/__init__.py | 3 + src/workos/types/user_management/__init__.py | 13 ++ .../types/user_management_users/__init__.py | 4 + src/workos/types/webhooks/__init__.py | 3 + src/workos/types/widgets/__init__.py | 3 + src/workos/types/workos_connect/__init__.py | 3 + ...r_authentication_factor_enroll_response.py | 1 + ...horized_connect_application_list_data.json | 2 +- tests/fixtures/event_schema.json | 32 +++ ...horized_connect_application_list_data.json | 2 +- tests/fixtures/list_event_schema.json | 40 ++++ tests/test_events.py | 17 ++ tests/test_models_round_trip.py | 201 ++++++++++++------ 47 files changed, 437 insertions(+), 132 deletions(-) create mode 100644 src/workos/events/models/event_schema.py create mode 100644 src/workos/exceptions.py create mode 100644 src/workos/types/__init__.py create mode 100644 src/workos/types/admin_portal/__init__.py create mode 100644 src/workos/types/api_keys/__init__.py create mode 100644 src/workos/types/application_client_secrets/__init__.py create mode 100644 src/workos/types/applications/__init__.py create mode 100644 src/workos/types/audit_logs/__init__.py create mode 100644 src/workos/types/authorization/__init__.py create mode 100644 src/workos/types/connections/__init__.py create mode 100644 src/workos/types/directories/__init__.py create mode 100644 src/workos/types/directory_groups/__init__.py create mode 100644 src/workos/types/directory_users/__init__.py create mode 100644 src/workos/types/events/__init__.py create mode 100644 src/workos/types/feature_flags/__init__.py create mode 100644 src/workos/types/multi_factor_auth/__init__.py create mode 100644 src/workos/types/organization_domains/__init__.py create mode 100644 src/workos/types/organizations/__init__.py create mode 100644 src/workos/types/permissions/__init__.py create mode 100644 src/workos/types/pipes/__init__.py create mode 100644 src/workos/types/radar/__init__.py create mode 100644 src/workos/types/sso/__init__.py create mode 100644 src/workos/types/user_management/__init__.py create mode 100644 src/workos/types/user_management_users/__init__.py create mode 100644 src/workos/types/webhooks/__init__.py create mode 100644 src/workos/types/widgets/__init__.py create mode 100644 src/workos/types/workos_connect/__init__.py create mode 100644 tests/fixtures/event_schema.json create mode 100644 tests/fixtures/list_event_schema.json diff --git a/docs/V6_MIGRATION_GUIDE.md b/docs/V6_MIGRATION_GUIDE.md index 1254e83a..576eb8bb 100644 --- a/docs/V6_MIGRATION_GUIDE.md +++ b/docs/V6_MIGRATION_GUIDE.md @@ -15,8 +15,6 @@ Read this as the migration guide for the generated-surface major release, not as - [Python 3.10+ is now required](#python-310-is-now-required) - [`client_id` is only required for flows that use it](#client_id-is-only-required-for-flows-that-use-it) - [Legacy client modules were removed](#legacy-client-modules-were-removed) - - [SDK exceptions now come from `workos`, not `workos.exceptions`](#sdk-exceptions-now-come-from-workos-not-workosexceptions) - - [Models no longer live under `workos.types.*`](#models-no-longer-live-under-workostypes) - [SDK models are no longer Pydantic models](#sdk-models-are-no-longer-pydantic-models) - [`client.connect` was split into explicit resources](#clientconnect-was-split-into-explicit-resources) - [`client.directory_sync` was split into directories, groups, and users](#clientdirectory_sync-was-split-into-directories-groups-and-users) @@ -36,7 +34,7 @@ Read this as the migration guide for the generated-surface major release, not as 1. Upgrade to Python 3.10 or newer. 2. Update to the v6 release. -3. Replace legacy imports from `workos.client`, `workos.async_client`, `workos.exceptions`, and `workos.types.*`. +3. Replace legacy imports from `workos.client` and `workos.async_client`. 4. Update any uses of `connect`, `portal`, `directory_sync`, `fga`, `authorization` permission helpers, and old `user_management` convenience helpers. 5. Run your tests and look specifically for import errors, missing methods, and model serialization issues. @@ -105,52 +103,6 @@ client = WorkOSClient(api_key="sk_test_123", client_id="client_123") **Affected users:** Anyone importing `SyncClient`, `AsyncClient`, or other client classes from legacy client modules **Migration:** Import clients from the top-level `workos` package instead of `workos.client` or `workos.async_client` -### SDK exceptions now come from `workos`, not `workos.exceptions` - -**5.x (old):** - -```python -from workos.exceptions import BaseRequestException - -try: - client.organizations.get_organization("org_123") -except BaseRequestException as exc: - print(exc.request_id) -``` - -**6.x (new):** - -```python -from workos import BaseRequestException - -try: - client.organizations.get("org_123") -except BaseRequestException as exc: - print(exc.request_id) -``` - -**Affected users:** Anyone importing exception classes from `workos.exceptions` -**Migration:** Import exception classes directly from `workos` - -### Models no longer live under `workos.types.*` - -**5.x (old):** - -```python -from workos.types.organizations import Organization -from workos.types.connect import ConnectApplication -``` - -**6.x (new):** - -```python -from workos.organizations.models import Organization -from workos.applications.models import ConnectApplication -``` - -**Affected users:** Anyone importing SDK models from `workos.types.*` -**Migration:** Import models from the generated `workos..models` packages - ### SDK models are no longer Pydantic models **5.x (old):** @@ -456,7 +408,7 @@ organization = client.organizations.get("org_123") 1. Upgrade Python to 3.10+. 2. Review which flows need `client_id` and either pass it explicitly or configure it on the client / environment. -3. Fix imports from `workos.client`, `workos.async_client`, `workos.exceptions`, and `workos.types.*`. +3. Fix imports from `workos.client` and `workos.async_client`. 4. Replace old namespaces: `connect`, `directory_sync`, `portal`, `fga`, and handwritten `user_management` helpers. 5. Move permission CRUD from `client.authorization` to `client.permissions`. 6. Update model serialization code away from Pydantic helpers. @@ -466,8 +418,6 @@ organization = client.organizations.get("org_123") - No imports from `workos.client` - No imports from `workos.async_client` -- No imports from `workos.exceptions` -- No imports from `workos.types.*` - Client-ID-based flows explicitly pass `client_id` or configure it on the client / environment - No remaining uses of `client.connect` - No remaining uses of `client.directory_sync` diff --git a/src/workos/audit_logs/models/audit_log_action_json.py b/src/workos/audit_logs/models/audit_log_action_json.py index f1b67d2d..bfccc5bf 100644 --- a/src/workos/audit_logs/models/audit_log_action_json.py +++ b/src/workos/audit_logs/models/audit_log_action_json.py @@ -20,6 +20,7 @@ class AuditLogActionJson: name: str """Identifier of what action was taken.""" schema: "AuditLogSchemaJson" + """The schema associated with the action.""" created_at: datetime """An ISO 8601 timestamp.""" updated_at: datetime diff --git a/src/workos/audit_logs/models/audit_log_schema_json.py b/src/workos/audit_logs/models/audit_log_schema_json.py index 0d0afba8..9143dad0 100644 --- a/src/workos/audit_logs/models/audit_log_schema_json.py +++ b/src/workos/audit_logs/models/audit_log_schema_json.py @@ -14,7 +14,7 @@ @dataclass(slots=True) class AuditLogSchemaJson: - """The schema associated with the action.""" + """Audit Log Schema Json model.""" object: Literal["audit_log_schema"] """Distinguishes the Audit Log Schema object.""" diff --git a/src/workos/authorization/models/role_assignment.py b/src/workos/authorization/models/role_assignment.py index ead5b127..a8819a3a 100644 --- a/src/workos/authorization/models/role_assignment.py +++ b/src/workos/authorization/models/role_assignment.py @@ -21,6 +21,7 @@ class RoleAssignment: id: str """Unique identifier of the role assignment.""" role: "SlimRole" + """The role included in the assignment.""" resource: "RoleAssignmentResource" """The resource to which the role is assigned.""" created_at: datetime diff --git a/src/workos/authorization/models/slim_role.py b/src/workos/authorization/models/slim_role.py index 7738aeb0..706fb334 100644 --- a/src/workos/authorization/models/slim_role.py +++ b/src/workos/authorization/models/slim_role.py @@ -9,7 +9,7 @@ @dataclass(slots=True) class SlimRole: - """The role included in the assignment.""" + """The primary role assigned to the user.""" slug: str """The slug of the assigned role.""" diff --git a/src/workos/directory_users/models/directory_user_with_groups.py b/src/workos/directory_users/models/directory_user_with_groups.py index 4c9ea24f..b2628aca 100644 --- a/src/workos/directory_users/models/directory_user_with_groups.py +++ b/src/workos/directory_users/models/directory_user_with_groups.py @@ -53,7 +53,6 @@ class DirectoryUserWithGroups: username: Optional[str] = None """The username of the user.""" role: Optional["SlimRole"] = None - """The primary role assigned to the user.""" roles: Optional[List["SlimRole"]] = None """All roles assigned to the user.""" diff --git a/src/workos/events/_resource.py b/src/workos/events/_resource.py index 077d7bd7..cb4b1189 100644 --- a/src/workos/events/_resource.py +++ b/src/workos/events/_resource.py @@ -8,7 +8,7 @@ from .._client import AsyncWorkOSClient, WorkOSClient from .._types import RequestOptions, enum_value -from .models import Event +from .models import EventSchema from .models import EventsOrder from .._pagination import AsyncPage, SyncPage @@ -31,7 +31,7 @@ def list( range_end: Optional[str] = None, organization_id: Optional[str] = None, request_options: Optional[RequestOptions] = None, - ) -> SyncPage[Event]: + ) -> SyncPage[EventSchema]: """List events List events for the current environment. @@ -48,7 +48,7 @@ def list( request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. Returns: - SyncPage[Event] + SyncPage[EventSchema] Raises: BadRequestException: If the request is malformed (400). @@ -74,7 +74,7 @@ def list( return self._client.request_page( method="get", path="events", - model=Event, + model=EventSchema, params=params, request_options=request_options, ) @@ -98,7 +98,7 @@ async def list( range_end: Optional[str] = None, organization_id: Optional[str] = None, request_options: Optional[RequestOptions] = None, - ) -> AsyncPage[Event]: + ) -> AsyncPage[EventSchema]: """List events List events for the current environment. @@ -115,7 +115,7 @@ async def list( request_options: Per-request options. Supports extra_headers, timeout, max_retries, and base_url override. Returns: - AsyncPage[Event] + AsyncPage[EventSchema] Raises: BadRequestException: If the request is malformed (400). @@ -141,7 +141,7 @@ async def list( return await self._client.request_page( method="get", path="events", - model=Event, + model=EventSchema, params=params, request_options=request_options, ) diff --git a/src/workos/events/models/__init__.py b/src/workos/events/models/__init__.py index 0116ecb9..e8d8bb8f 100644 --- a/src/workos/events/models/__init__.py +++ b/src/workos/events/models/__init__.py @@ -1,4 +1,4 @@ # This file is auto-generated by oagen. Do not edit. -from .event import Event as Event +from .event_schema import EventSchema as EventSchema from .events_order import EventsOrder as EventsOrder diff --git a/src/workos/events/models/event_schema.py b/src/workos/events/models/event_schema.py new file mode 100644 index 00000000..06d0da45 --- /dev/null +++ b/src/workos/events/models/event_schema.py @@ -0,0 +1,59 @@ +# This file is auto-generated by oagen. Do not edit. + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Any, Dict, Literal, Optional +from workos._errors import BaseRequestException + + +@dataclass(slots=True) +class EventSchema: + """An event emitted by WorkOS.""" + + object: Literal["event"] + """Distinguishes the Event object.""" + id: str + """Unique identifier for the Event.""" + event: str + """The type of event that occurred.""" + data: Dict[str, Any] + """The event payload.""" + created_at: datetime + """An ISO 8601 timestamp.""" + context: Optional[Dict[str, Any]] = None + """Additional context about the event.""" + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "EventSchema": + """Deserialize from a dictionary.""" + try: + return cls( + object=data["object"], + id=data["id"], + event=data["event"], + data=data["data"], + created_at=datetime.fromisoformat( + data["created_at"].replace("Z", "+00:00") + ), + context=data.get("context"), + ) + except (KeyError, ValueError) as e: + raise BaseRequestException( + f"Unexpected API response while parsing EventSchema: {e!s}" + ) from e + + def to_dict(self) -> Dict[str, Any]: + """Serialize to a dictionary.""" + result: Dict[str, Any] = {} + result["object"] = self.object + result["id"] = self.id + result["event"] = self.event + result["data"] = self.data + result["created_at"] = self.created_at.isoformat( + timespec="milliseconds" + ).replace("+00:00", "Z") + if self.context is not None: + result["context"] = self.context + return result diff --git a/src/workos/exceptions.py b/src/workos/exceptions.py new file mode 100644 index 00000000..fd61c877 --- /dev/null +++ b/src/workos/exceptions.py @@ -0,0 +1,41 @@ +# This file is auto-generated by oagen. Do not edit. + +"""Backwards-compatible re-export of exception classes. + +Allows 'from workos.exceptions import ...' alongside the +canonical 'from workos import ...' path. +""" + +from ._errors import ( + BaseRequestException as BaseRequestException, + AuthenticationException as AuthenticationException, + AuthorizationException as AuthorizationException, + BadRequestException as BadRequestException, + ConflictException as ConflictException, + ConfigurationException as ConfigurationException, + EmailVerificationRequiredException as EmailVerificationRequiredException, + NotFoundException as NotFoundException, + RateLimitExceededException as RateLimitExceededException, + ServerException as ServerException, + UnprocessableEntityException as UnprocessableEntityException, + WorkOSConnectionException as WorkOSConnectionException, + WorkOSTimeoutException as WorkOSTimeoutException, + STATUS_CODE_TO_EXCEPTION as STATUS_CODE_TO_EXCEPTION, +) + +__all__ = [ + "BaseRequestException", + "AuthenticationException", + "AuthorizationException", + "BadRequestException", + "ConflictException", + "ConfigurationException", + "EmailVerificationRequiredException", + "NotFoundException", + "RateLimitExceededException", + "ServerException", + "UnprocessableEntityException", + "WorkOSConnectionException", + "WorkOSTimeoutException", + "STATUS_CODE_TO_EXCEPTION", +] diff --git a/src/workos/multi_factor_auth/challenges/models/authentication_challenge.py b/src/workos/multi_factor_auth/challenges/models/authentication_challenge.py index 370fede1..75c47088 100644 --- a/src/workos/multi_factor_auth/challenges/models/authentication_challenge.py +++ b/src/workos/multi_factor_auth/challenges/models/authentication_challenge.py @@ -10,7 +10,7 @@ @dataclass(slots=True) class AuthenticationChallenge: - """The authentication challenge object.""" + """Authentication Challenge model.""" object: Literal["authentication_challenge"] """Distinguishes the authentication challenge object.""" diff --git a/src/workos/multi_factor_auth/challenges/models/authentication_challenge_verify_response.py b/src/workos/multi_factor_auth/challenges/models/authentication_challenge_verify_response.py index 756e57bf..36fc2700 100644 --- a/src/workos/multi_factor_auth/challenges/models/authentication_challenge_verify_response.py +++ b/src/workos/multi_factor_auth/challenges/models/authentication_challenge_verify_response.py @@ -15,6 +15,7 @@ class AuthenticationChallengeVerifyResponse: """Authentication Challenge Verify Response model.""" challenge: "AuthenticationChallenge" + """The authentication challenge object.""" valid: bool """Whether the code was valid.""" diff --git a/src/workos/multi_factor_auth/models/authentication_factor_enrolled.py b/src/workos/multi_factor_auth/models/authentication_factor_enrolled.py index 175bee8e..693c648e 100644 --- a/src/workos/multi_factor_auth/models/authentication_factor_enrolled.py +++ b/src/workos/multi_factor_auth/models/authentication_factor_enrolled.py @@ -15,7 +15,7 @@ @dataclass(slots=True) class AuthenticationFactorEnrolled: - """The [authentication factor](https://workos.com/docs/reference/authkit/mfa/authentication-factor) object that represents the additional authentication method used on top of the existing authentication strategy.""" + """Authentication Factor Enrolled model.""" object: Literal["authentication_factor"] """Distinguishes the authentication factor object.""" diff --git a/src/workos/sso/models/profile.py b/src/workos/sso/models/profile.py index 67d52e20..4d70111c 100644 --- a/src/workos/sso/models/profile.py +++ b/src/workos/sso/models/profile.py @@ -13,7 +13,7 @@ @dataclass(slots=True) class Profile: - """The user profile returned by the identity provider.""" + """Profile model.""" object: Literal["profile"] """Distinguishes the profile object.""" diff --git a/src/workos/sso/models/sso_token_response.py b/src/workos/sso/models/sso_token_response.py index 84a8e83d..4c5bed63 100644 --- a/src/workos/sso/models/sso_token_response.py +++ b/src/workos/sso/models/sso_token_response.py @@ -22,6 +22,7 @@ class SSOTokenResponse: expires_in: int """The lifetime of the access token in seconds.""" profile: "Profile" + """The user profile returned by the identity provider.""" oauth_tokens: Optional["SSOTokenResponseOAuthToken"] = None """OAuth tokens issued by the identity provider, if available.""" diff --git a/src/workos/types/__init__.py b/src/workos/types/__init__.py new file mode 100644 index 00000000..33e9c7b7 --- /dev/null +++ b/src/workos/types/__init__.py @@ -0,0 +1,2 @@ +# This file is auto-generated by oagen. Do not edit. + diff --git a/src/workos/types/admin_portal/__init__.py b/src/workos/types/admin_portal/__init__.py new file mode 100644 index 00000000..d183d123 --- /dev/null +++ b/src/workos/types/admin_portal/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.admin_portal.models import * # noqa: F401,F403 diff --git a/src/workos/types/api_keys/__init__.py b/src/workos/types/api_keys/__init__.py new file mode 100644 index 00000000..d83f31ba --- /dev/null +++ b/src/workos/types/api_keys/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.api_keys.models import * # noqa: F401,F403 diff --git a/src/workos/types/application_client_secrets/__init__.py b/src/workos/types/application_client_secrets/__init__.py new file mode 100644 index 00000000..cdce13dd --- /dev/null +++ b/src/workos/types/application_client_secrets/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.application_client_secrets.models import * # noqa: F401,F403 diff --git a/src/workos/types/applications/__init__.py b/src/workos/types/applications/__init__.py new file mode 100644 index 00000000..9183ef05 --- /dev/null +++ b/src/workos/types/applications/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.applications.models import * # noqa: F401,F403 diff --git a/src/workos/types/audit_logs/__init__.py b/src/workos/types/audit_logs/__init__.py new file mode 100644 index 00000000..7a55f4aa --- /dev/null +++ b/src/workos/types/audit_logs/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.audit_logs.models import * # noqa: F401,F403 diff --git a/src/workos/types/authorization/__init__.py b/src/workos/types/authorization/__init__.py new file mode 100644 index 00000000..599e9b5a --- /dev/null +++ b/src/workos/types/authorization/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.authorization.models import * # noqa: F401,F403 diff --git a/src/workos/types/connections/__init__.py b/src/workos/types/connections/__init__.py new file mode 100644 index 00000000..d8bef48a --- /dev/null +++ b/src/workos/types/connections/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.connections.models import * # noqa: F401,F403 diff --git a/src/workos/types/directories/__init__.py b/src/workos/types/directories/__init__.py new file mode 100644 index 00000000..ffced000 --- /dev/null +++ b/src/workos/types/directories/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.directories.models import * # noqa: F401,F403 diff --git a/src/workos/types/directory_groups/__init__.py b/src/workos/types/directory_groups/__init__.py new file mode 100644 index 00000000..c257566b --- /dev/null +++ b/src/workos/types/directory_groups/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.directory_groups.models import * # noqa: F401,F403 diff --git a/src/workos/types/directory_users/__init__.py b/src/workos/types/directory_users/__init__.py new file mode 100644 index 00000000..2a5bae9e --- /dev/null +++ b/src/workos/types/directory_users/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.directory_users.models import * # noqa: F401,F403 diff --git a/src/workos/types/events/__init__.py b/src/workos/types/events/__init__.py new file mode 100644 index 00000000..89fa42fe --- /dev/null +++ b/src/workos/types/events/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.events.models import * # noqa: F401,F403 diff --git a/src/workos/types/feature_flags/__init__.py b/src/workos/types/feature_flags/__init__.py new file mode 100644 index 00000000..63d36499 --- /dev/null +++ b/src/workos/types/feature_flags/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.feature_flags.models import * # noqa: F401,F403 +from workos.feature_flags.targets.models import * # noqa: F401,F403 diff --git a/src/workos/types/multi_factor_auth/__init__.py b/src/workos/types/multi_factor_auth/__init__.py new file mode 100644 index 00000000..cdc2ad05 --- /dev/null +++ b/src/workos/types/multi_factor_auth/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.multi_factor_auth.models import * # noqa: F401,F403 +from workos.multi_factor_auth.challenges.models import * # noqa: F401,F403 diff --git a/src/workos/types/organization_domains/__init__.py b/src/workos/types/organization_domains/__init__.py new file mode 100644 index 00000000..aa31be5f --- /dev/null +++ b/src/workos/types/organization_domains/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.organization_domains.models import * # noqa: F401,F403 diff --git a/src/workos/types/organizations/__init__.py b/src/workos/types/organizations/__init__.py new file mode 100644 index 00000000..cd384055 --- /dev/null +++ b/src/workos/types/organizations/__init__.py @@ -0,0 +1,5 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.organizations.models import * # noqa: F401,F403 +from workos.organizations.api_keys.models import * # noqa: F401,F403 +from workos.organizations.feature_flags.models import * # noqa: F401,F403 diff --git a/src/workos/types/permissions/__init__.py b/src/workos/types/permissions/__init__.py new file mode 100644 index 00000000..6c9dc9d4 --- /dev/null +++ b/src/workos/types/permissions/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.permissions.models import * # noqa: F401,F403 diff --git a/src/workos/types/pipes/__init__.py b/src/workos/types/pipes/__init__.py new file mode 100644 index 00000000..dd3c36bd --- /dev/null +++ b/src/workos/types/pipes/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.pipes.models import * # noqa: F401,F403 diff --git a/src/workos/types/radar/__init__.py b/src/workos/types/radar/__init__.py new file mode 100644 index 00000000..78661b8a --- /dev/null +++ b/src/workos/types/radar/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.radar.models import * # noqa: F401,F403 diff --git a/src/workos/types/sso/__init__.py b/src/workos/types/sso/__init__.py new file mode 100644 index 00000000..c16bc168 --- /dev/null +++ b/src/workos/types/sso/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.sso.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management/__init__.py b/src/workos/types/user_management/__init__.py new file mode 100644 index 00000000..28030de7 --- /dev/null +++ b/src/workos/types/user_management/__init__.py @@ -0,0 +1,13 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management.authentication.models import * # noqa: F401,F403 +from workos.user_management.cors_origins.models import * # noqa: F401,F403 +from workos.user_management.data_providers.models import * # noqa: F401,F403 +from workos.user_management.invitations.models import * # noqa: F401,F403 +from workos.user_management.jwt_template.models import * # noqa: F401,F403 +from workos.user_management.magic_auth.models import * # noqa: F401,F403 +from workos.user_management.multi_factor_authentication.models import * # noqa: F401,F403 +from workos.user_management.organization_membership.models import * # noqa: F401,F403 +from workos.user_management.redirect_uris.models import * # noqa: F401,F403 +from workos.user_management.session_tokens.models import * # noqa: F401,F403 +from workos.user_management.users.models import * # noqa: F401,F403 diff --git a/src/workos/types/user_management_users/__init__.py b/src/workos/types/user_management_users/__init__.py new file mode 100644 index 00000000..2d31c22c --- /dev/null +++ b/src/workos/types/user_management_users/__init__.py @@ -0,0 +1,4 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.user_management_users.authorized_applications.models import * # noqa: F401,F403 +from workos.user_management_users.feature_flags.models import * # noqa: F401,F403 diff --git a/src/workos/types/webhooks/__init__.py b/src/workos/types/webhooks/__init__.py new file mode 100644 index 00000000..0b47a41b --- /dev/null +++ b/src/workos/types/webhooks/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.webhooks.models import * # noqa: F401,F403 diff --git a/src/workos/types/widgets/__init__.py b/src/workos/types/widgets/__init__.py new file mode 100644 index 00000000..0daafdc6 --- /dev/null +++ b/src/workos/types/widgets/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.widgets.models import * # noqa: F401,F403 diff --git a/src/workos/types/workos_connect/__init__.py b/src/workos/types/workos_connect/__init__.py new file mode 100644 index 00000000..96cf742d --- /dev/null +++ b/src/workos/types/workos_connect/__init__.py @@ -0,0 +1,3 @@ +# This file is auto-generated by oagen. Do not edit. + +from workos.workos_connect.models import * # noqa: F401,F403 diff --git a/src/workos/user_management/multi_factor_authentication/models/user_authentication_factor_enroll_response.py b/src/workos/user_management/multi_factor_authentication/models/user_authentication_factor_enroll_response.py index c5bb74e3..a2d2278b 100644 --- a/src/workos/user_management/multi_factor_authentication/models/user_authentication_factor_enroll_response.py +++ b/src/workos/user_management/multi_factor_authentication/models/user_authentication_factor_enroll_response.py @@ -16,6 +16,7 @@ class UserAuthenticationFactorEnrollResponse: """User Authentication Factor Enroll Response model.""" authentication_factor: "AuthenticationFactorEnrolled" + """The [authentication factor](https://workos.com/docs/reference/authkit/mfa/authentication-factor) object that represents the additional authentication method used on top of the existing authentication strategy.""" authentication_challenge: "AuthenticationChallenge" """The [authentication challenge](https://workos.com/docs/reference/authkit/mfa/authentication-challenge) object that is used to complete the authentication process.""" diff --git a/tests/fixtures/authorized_connect_application_list_data.json b/tests/fixtures/authorized_connect_application_list_data.json index 0132aa02..bd3d6a14 100644 --- a/tests/fixtures/authorized_connect_application_list_data.json +++ b/tests/fixtures/authorized_connect_application_list_data.json @@ -1,6 +1,6 @@ { "object": "authorized_connect_application", - "id": "aca_01HXYZ123456789ABCDEFGHIJ", + "id": "authorized_connect_app_01HXYZ123456789ABCDEFGHIJ", "granted_scopes": [ "openid", "profile", diff --git a/tests/fixtures/event_schema.json b/tests/fixtures/event_schema.json new file mode 100644 index 00000000..f3c3b571 --- /dev/null +++ b/tests/fixtures/event_schema.json @@ -0,0 +1,32 @@ +{ + "object": "event", + "id": "event_01EHZNVPK3SFK441A1RGBFSHRT", + "event": "dsync.user.created", + "data": { + "id": "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", + "state": "active", + "email": "veda@foo-corp.com", + "emails": [ + { + "primary": true, + "type": "work", + "value": "veda@foo-corp.com" + } + ], + "idp_id": "2836", + "object": "directory_user", + "username": "veda@foo-corp.com", + "last_name": "Torp", + "first_name": "Veda", + "raw_attributes": {}, + "custom_attributes": {}, + "created_at": "2021-06-25T19:07:33.155Z", + "updated_at": "2021-06-25T19:07:33.155Z" + }, + "created_at": "2026-01-15T12:00:00.000Z", + "context": { + "key": {} + } +} diff --git a/tests/fixtures/list_authorized_connect_application_list_data.json b/tests/fixtures/list_authorized_connect_application_list_data.json index 198e9221..d1f30827 100644 --- a/tests/fixtures/list_authorized_connect_application_list_data.json +++ b/tests/fixtures/list_authorized_connect_application_list_data.json @@ -2,7 +2,7 @@ "data": [ { "object": "authorized_connect_application", - "id": "aca_01HXYZ123456789ABCDEFGHIJ", + "id": "authorized_connect_app_01HXYZ123456789ABCDEFGHIJ", "granted_scopes": [ "openid", "profile", diff --git a/tests/fixtures/list_event_schema.json b/tests/fixtures/list_event_schema.json new file mode 100644 index 00000000..cd8e9bb3 --- /dev/null +++ b/tests/fixtures/list_event_schema.json @@ -0,0 +1,40 @@ +{ + "data": [ + { + "object": "event", + "id": "event_01EHZNVPK3SFK441A1RGBFSHRT", + "event": "dsync.user.created", + "data": { + "id": "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", + "state": "active", + "email": "veda@foo-corp.com", + "emails": [ + { + "primary": true, + "type": "work", + "value": "veda@foo-corp.com" + } + ], + "idp_id": "2836", + "object": "directory_user", + "username": "veda@foo-corp.com", + "last_name": "Torp", + "first_name": "Veda", + "raw_attributes": {}, + "custom_attributes": {}, + "created_at": "2021-06-25T19:07:33.155Z", + "updated_at": "2021-06-25T19:07:33.155Z" + }, + "created_at": "2026-01-15T12:00:00.000Z", + "context": { + "key": {} + } + } + ], + "list_metadata": { + "before": null, + "after": null + } +} diff --git a/tests/test_events.py b/tests/test_events.py index bd464407..bd2a944f 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -3,6 +3,7 @@ import pytest from workos import WorkOS, AsyncWorkOS +from tests.generated_helpers import load_fixture from workos.events.models import EventsOrder from workos._pagination import AsyncPage, SyncPage @@ -16,9 +17,18 @@ class TestEvents: def test_list(self, workos, httpx_mock): + httpx_mock.add_response( + json=load_fixture("list_event_schema.json"), + ) + page = workos.events.list() + assert isinstance(page, SyncPage) + assert isinstance(page.data, list) + + def test_list_empty_page(self, workos, httpx_mock): httpx_mock.add_response(json={"data": [], "list_metadata": {}}) page = workos.events.list() assert isinstance(page, SyncPage) + assert page.data == [] def test_list_encodes_query_params(self, workos, httpx_mock): httpx_mock.add_response(json={"data": [], "list_metadata": {}}) @@ -83,9 +93,16 @@ def test_list_server_error(self, httpx_mock): @pytest.mark.asyncio class TestAsyncEvents: async def test_list(self, async_workos, httpx_mock): + httpx_mock.add_response(json=load_fixture("list_event_schema.json")) + page = await async_workos.events.list() + assert isinstance(page, AsyncPage) + assert isinstance(page.data, list) + + async def test_list_empty_page(self, async_workos, httpx_mock): httpx_mock.add_response(json={"data": [], "list_metadata": {}}) page = await async_workos.events.list() assert isinstance(page, AsyncPage) + assert page.data == [] async def test_list_encodes_query_params(self, async_workos, httpx_mock): httpx_mock.add_response(json={"data": [], "list_metadata": {}}) diff --git a/tests/test_models_round_trip.py b/tests/test_models_round_trip.py index e9907082..53085e8b 100644 --- a/tests/test_models_round_trip.py +++ b/tests/test_models_round_trip.py @@ -52,6 +52,7 @@ DirectoryUserWithGroups, DirectoryUserWithGroupsEmail, ) +from workos.events.models import EventSchema from workos.feature_flags.models import FeatureFlag, FeatureFlagOwner, Flag, FlagOwner from workos.multi_factor_auth.models import ( AuthenticationFactor, @@ -1626,85 +1627,77 @@ def test_directory_user_with_groups_round_trips_unknown_enum_values(self): instance = DirectoryUserWithGroups.from_dict(data) assert instance.to_dict() == data - def test_user_round_trip(self): - data = load_fixture("user.json") - instance = User.from_dict(data) + def test_event_schema_round_trip(self): + data = load_fixture("event_schema.json") + instance = EventSchema.from_dict(data) serialized = instance.to_dict() assert serialized == data - restored = User.from_dict(serialized) + restored = EventSchema.from_dict(serialized) assert restored.to_dict() == serialized - def test_user_minimal_payload(self): + def test_event_schema_minimal_payload(self): data = { - "object": "user", - "id": "user_01E4ZCR3C56J083X43JQXF3JK5", - "first_name": None, - "last_name": None, - "profile_picture_url": None, - "email": "marcelina.davis@example.com", - "email_verified": True, - "external_id": None, - "last_sign_in_at": None, + "object": "event", + "id": "event_01EHZNVPK3SFK441A1RGBFSHRT", + "event": "dsync.user.created", + "data": { + "id": "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", + "state": "active", + "email": "veda@foo-corp.com", + "emails": [ + {"primary": True, "type": "work", "value": "veda@foo-corp.com"} + ], + "idp_id": "2836", + "object": "directory_user", + "username": "veda@foo-corp.com", + "last_name": "Torp", + "first_name": "Veda", + "raw_attributes": {}, + "custom_attributes": {}, + "created_at": "2021-06-25T19:07:33.155Z", + "updated_at": "2021-06-25T19:07:33.155Z", + }, "created_at": "2026-01-15T12:00:00.000Z", - "updated_at": "2026-01-15T12:00:00.000Z", } - instance = User.from_dict(data) + instance = EventSchema.from_dict(data) serialized = instance.to_dict() assert serialized["object"] == data["object"] assert serialized["id"] == data["id"] - assert serialized["first_name"] == data["first_name"] - assert serialized["last_name"] == data["last_name"] - assert serialized["profile_picture_url"] == data["profile_picture_url"] - assert serialized["email"] == data["email"] - assert serialized["email_verified"] == data["email_verified"] - assert serialized["external_id"] == data["external_id"] - assert serialized["last_sign_in_at"] == data["last_sign_in_at"] + assert serialized["event"] == data["event"] + assert serialized["data"] == data["data"] assert serialized["created_at"] == data["created_at"] - assert serialized["updated_at"] == data["updated_at"] - def test_user_omits_absent_optional_non_nullable_fields(self): + def test_event_schema_omits_absent_optional_non_nullable_fields(self): data = { - "object": "user", - "id": "user_01E4ZCR3C56J083X43JQXF3JK5", - "first_name": "Marcelina", - "last_name": "Davis", - "profile_picture_url": "https://workoscdn.com/images/v1/123abc", - "email": "marcelina.davis@example.com", - "email_verified": True, - "external_id": "f1ffa2b2-c20b-4d39-be5c-212726e11222", - "last_sign_in_at": "2025-06-25T19:07:33.155Z", - "locale": "en-US", - "created_at": "2026-01-15T12:00:00.000Z", - "updated_at": "2026-01-15T12:00:00.000Z", - } - instance = User.from_dict(data) - serialized = instance.to_dict() - assert "metadata" not in serialized - - def test_user_preserves_nullable_fields(self): - data = { - "object": "user", - "id": "user_01E4ZCR3C56J083X43JQXF3JK5", - "first_name": None, - "last_name": None, - "profile_picture_url": None, - "email": "marcelina.davis@example.com", - "email_verified": True, - "external_id": None, - "metadata": {"timezone": "America/New_York"}, - "last_sign_in_at": None, - "locale": None, + "object": "event", + "id": "event_01EHZNVPK3SFK441A1RGBFSHRT", + "event": "dsync.user.created", + "data": { + "id": "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ", + "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74", + "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y", + "state": "active", + "email": "veda@foo-corp.com", + "emails": [ + {"primary": True, "type": "work", "value": "veda@foo-corp.com"} + ], + "idp_id": "2836", + "object": "directory_user", + "username": "veda@foo-corp.com", + "last_name": "Torp", + "first_name": "Veda", + "raw_attributes": {}, + "custom_attributes": {}, + "created_at": "2021-06-25T19:07:33.155Z", + "updated_at": "2021-06-25T19:07:33.155Z", + }, "created_at": "2026-01-15T12:00:00.000Z", - "updated_at": "2026-01-15T12:00:00.000Z", } - instance = User.from_dict(data) + instance = EventSchema.from_dict(data) serialized = instance.to_dict() - assert serialized["first_name"] is None - assert serialized["last_name"] is None - assert serialized["profile_picture_url"] is None - assert serialized["external_id"] is None - assert serialized["last_sign_in_at"] is None - assert serialized["locale"] is None + assert "context" not in serialized def test_jwt_template_response_round_trip(self): data = load_fixture("jwt_template_response.json") @@ -2493,6 +2486,86 @@ def test_user_organization_membership_round_trips_unknown_enum_values(self): instance = UserOrganizationMembership.from_dict(data) assert instance.to_dict() == data + def test_user_round_trip(self): + data = load_fixture("user.json") + instance = User.from_dict(data) + serialized = instance.to_dict() + assert serialized == data + restored = User.from_dict(serialized) + assert restored.to_dict() == serialized + + def test_user_minimal_payload(self): + data = { + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": None, + "last_name": None, + "profile_picture_url": None, + "email": "marcelina.davis@example.com", + "email_verified": True, + "external_id": None, + "last_sign_in_at": None, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = User.from_dict(data) + serialized = instance.to_dict() + assert serialized["object"] == data["object"] + assert serialized["id"] == data["id"] + assert serialized["first_name"] == data["first_name"] + assert serialized["last_name"] == data["last_name"] + assert serialized["profile_picture_url"] == data["profile_picture_url"] + assert serialized["email"] == data["email"] + assert serialized["email_verified"] == data["email_verified"] + assert serialized["external_id"] == data["external_id"] + assert serialized["last_sign_in_at"] == data["last_sign_in_at"] + assert serialized["created_at"] == data["created_at"] + assert serialized["updated_at"] == data["updated_at"] + + def test_user_omits_absent_optional_non_nullable_fields(self): + data = { + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": "Marcelina", + "last_name": "Davis", + "profile_picture_url": "https://workoscdn.com/images/v1/123abc", + "email": "marcelina.davis@example.com", + "email_verified": True, + "external_id": "f1ffa2b2-c20b-4d39-be5c-212726e11222", + "last_sign_in_at": "2025-06-25T19:07:33.155Z", + "locale": "en-US", + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = User.from_dict(data) + serialized = instance.to_dict() + assert "metadata" not in serialized + + def test_user_preserves_nullable_fields(self): + data = { + "object": "user", + "id": "user_01E4ZCR3C56J083X43JQXF3JK5", + "first_name": None, + "last_name": None, + "profile_picture_url": None, + "email": "marcelina.davis@example.com", + "email_verified": True, + "external_id": None, + "metadata": {"timezone": "America/New_York"}, + "last_sign_in_at": None, + "locale": None, + "created_at": "2026-01-15T12:00:00.000Z", + "updated_at": "2026-01-15T12:00:00.000Z", + } + instance = User.from_dict(data) + serialized = instance.to_dict() + assert serialized["first_name"] is None + assert serialized["last_name"] is None + assert serialized["profile_picture_url"] is None + assert serialized["external_id"] is None + assert serialized["last_sign_in_at"] is None + assert serialized["locale"] is None + def test_email_verification_round_trip(self): data = load_fixture("email_verification.json") instance = EmailVerification.from_dict(data) @@ -3675,7 +3748,7 @@ def test_authorized_connect_application_list_data_round_trip(self): def test_authorized_connect_application_list_data_minimal_payload(self): data = { "object": "authorized_connect_application", - "id": "aca_01HXYZ123456789ABCDEFGHIJ", + "id": "authorized_connect_app_01HXYZ123456789ABCDEFGHIJ", "granted_scopes": ["openid", "profile", "email"], "application": { "object": "connect_application",