From 777de0fe66b85660e24847c55779e8b3dc2c1945 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:39:30 +0000 Subject: [PATCH 01/12] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index f6bc2e9b..4d056b8b 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 645 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gcore%2Fgcore-2d64161889a53046ca5e593a772328a5290246c3d74f8c07fed1037feae1bdbc.yml -openapi_spec_hash: 2360d25764d74e6972cf600fcd20fd4e +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gcore%2Fgcore-61da4ea9147efbd920bf7eb3e4c790787145f67e9e119a5269a358d48fa83d92.yml +openapi_spec_hash: 49f7e7bd1cde8953248eecea9ecc84df config_hash: e9e5b750687e9071d8b606963f0ffd6d From a2328c02b6d94f8af8cdeb7dbc72d81f1e2d17d0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:15:12 +0000 Subject: [PATCH 02/12] chore(ci): upgrade `actions/github-script` --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21d8baf0..30235252 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: - name: Get GitHub OIDC Token if: github.repository == 'stainless-sdks/gcore-python' id: github-oidc - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); From a7076d04fa78e4ea71b148ef8c10d0013536e904 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 08:15:06 +0000 Subject: [PATCH 03/12] feat(api): aggregated API specs update --- .stats.yml | 4 ++-- src/gcore/types/cdn/cdn_resource.py | 9 +++++++-- src/gcore/types/cdn/resource_create_params.py | 9 +++++++-- src/gcore/types/cdn/resource_replace_params.py | 9 +++++++-- src/gcore/types/cdn/resource_update_params.py | 9 +++++++-- src/gcore/types/cdn/resources/cdn_resource_rule.py | 9 +++++++-- src/gcore/types/cdn/resources/rule_create_params.py | 9 +++++++-- src/gcore/types/cdn/resources/rule_replace_params.py | 9 +++++++-- src/gcore/types/cdn/resources/rule_update_params.py | 9 +++++++-- src/gcore/types/cdn/rule_template.py | 9 +++++++-- src/gcore/types/cdn/rule_template_create_params.py | 9 +++++++-- src/gcore/types/cdn/rule_template_replace_params.py | 9 +++++++-- src/gcore/types/cdn/rule_template_update_params.py | 9 +++++++-- tests/api_resources/cdn/resources/test_rules.py | 12 ++++++------ tests/api_resources/cdn/test_resources.py | 12 ++++++------ tests/api_resources/cdn/test_rule_templates.py | 12 ++++++------ 16 files changed, 104 insertions(+), 44 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4d056b8b..cd2e413f 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 645 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gcore%2Fgcore-61da4ea9147efbd920bf7eb3e4c790787145f67e9e119a5269a358d48fa83d92.yml -openapi_spec_hash: 49f7e7bd1cde8953248eecea9ecc84df +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gcore%2Fgcore-dc42eef367287a7e7bcd1a562a519594fd3ac1b50ee86cfd8c6337b334dfbf25.yml +openapi_spec_hash: e72c6e3d4a64c6fc34a6b73431e65fba config_hash: e9e5b750687e9071d8b606963f0ffd6d diff --git a/src/gcore/types/cdn/cdn_resource.py b/src/gcore/types/cdn/cdn_resource.py index a1066729..760e9ee1 100644 --- a/src/gcore/types/cdn/cdn_resource.py +++ b/src/gcore/types/cdn/cdn_resource.py @@ -790,6 +790,7 @@ class OptionsIPAddressACL(BaseModel): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, you have to independently monitor their relevance. + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ @@ -1592,6 +1593,9 @@ class OptionsUserAgentACL(BaseModel): - **allow** - List of User-Agents for which access is prohibited. - **deny** - List of User-Agents for which access is allowed. + You can provide exact User-Agent strings or regular expressions. Regular + expressions must start with `~` (case-sensitive) or `~*` (case-insensitive). + Use an empty string `""` to allow/deny access when the User-Agent header is empty. """ @@ -1823,8 +1827,9 @@ class Options(BaseModel): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, - you have to independently monitor their relevance. We recommend you use a script - for automatically update IP ACL. + you have to independently monitor their relevance. + + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ diff --git a/src/gcore/types/cdn/resource_create_params.py b/src/gcore/types/cdn/resource_create_params.py index 09c122d2..743fba2a 100644 --- a/src/gcore/types/cdn/resource_create_params.py +++ b/src/gcore/types/cdn/resource_create_params.py @@ -913,6 +913,7 @@ class OptionsIPAddressACL(TypedDict, total=False): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, you have to independently monitor their relevance. + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ @@ -1713,6 +1714,9 @@ class OptionsUserAgentACL(TypedDict, total=False): - **allow** - List of User-Agents for which access is prohibited. - **deny** - List of User-Agents for which access is allowed. + You can provide exact User-Agent strings or regular expressions. Regular + expressions must start with `~` (case-sensitive) or `~*` (case-insensitive). + Use an empty string `""` to allow/deny access when the User-Agent header is empty. """ @@ -1944,8 +1948,9 @@ class Options(TypedDict, total=False): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, - you have to independently monitor their relevance. We recommend you use a script - for automatically update IP ACL. + you have to independently monitor their relevance. + + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ diff --git a/src/gcore/types/cdn/resource_replace_params.py b/src/gcore/types/cdn/resource_replace_params.py index e1ab08b8..fb57bb77 100644 --- a/src/gcore/types/cdn/resource_replace_params.py +++ b/src/gcore/types/cdn/resource_replace_params.py @@ -891,6 +891,7 @@ class OptionsIPAddressACL(TypedDict, total=False): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, you have to independently monitor their relevance. + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ @@ -1691,6 +1692,9 @@ class OptionsUserAgentACL(TypedDict, total=False): - **allow** - List of User-Agents for which access is prohibited. - **deny** - List of User-Agents for which access is allowed. + You can provide exact User-Agent strings or regular expressions. Regular + expressions must start with `~` (case-sensitive) or `~*` (case-insensitive). + Use an empty string `""` to allow/deny access when the User-Agent header is empty. """ @@ -1922,8 +1926,9 @@ class Options(TypedDict, total=False): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, - you have to independently monitor their relevance. We recommend you use a script - for automatically update IP ACL. + you have to independently monitor their relevance. + + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ diff --git a/src/gcore/types/cdn/resource_update_params.py b/src/gcore/types/cdn/resource_update_params.py index c231f15d..92251e5c 100644 --- a/src/gcore/types/cdn/resource_update_params.py +++ b/src/gcore/types/cdn/resource_update_params.py @@ -882,6 +882,7 @@ class OptionsIPAddressACL(TypedDict, total=False): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, you have to independently monitor their relevance. + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ @@ -1682,6 +1683,9 @@ class OptionsUserAgentACL(TypedDict, total=False): - **allow** - List of User-Agents for which access is prohibited. - **deny** - List of User-Agents for which access is allowed. + You can provide exact User-Agent strings or regular expressions. Regular + expressions must start with `~` (case-sensitive) or `~*` (case-insensitive). + Use an empty string `""` to allow/deny access when the User-Agent header is empty. """ @@ -1913,8 +1917,9 @@ class Options(TypedDict, total=False): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, - you have to independently monitor their relevance. We recommend you use a script - for automatically update IP ACL. + you have to independently monitor their relevance. + + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ diff --git a/src/gcore/types/cdn/resources/cdn_resource_rule.py b/src/gcore/types/cdn/resources/cdn_resource_rule.py index 2b61a6d2..c28f6e6a 100644 --- a/src/gcore/types/cdn/resources/cdn_resource_rule.py +++ b/src/gcore/types/cdn/resources/cdn_resource_rule.py @@ -762,6 +762,7 @@ class OptionsIPAddressACL(BaseModel): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, you have to independently monitor their relevance. + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ @@ -1474,6 +1475,9 @@ class OptionsUserAgentACL(BaseModel): - **allow** - List of User-Agents for which access is prohibited. - **deny** - List of User-Agents for which access is allowed. + You can provide exact User-Agent strings or regular expressions. Regular + expressions must start with `~` (case-sensitive) or `~*` (case-insensitive). + Use an empty string `""` to allow/deny access when the User-Agent header is empty. """ @@ -1699,8 +1703,9 @@ class Options(BaseModel): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, - you have to independently monitor their relevance. We recommend you use a script - for automatically update IP ACL. + you have to independently monitor their relevance. + + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ diff --git a/src/gcore/types/cdn/resources/rule_create_params.py b/src/gcore/types/cdn/resources/rule_create_params.py index 9d48700f..052803bb 100644 --- a/src/gcore/types/cdn/resources/rule_create_params.py +++ b/src/gcore/types/cdn/resources/rule_create_params.py @@ -838,6 +838,7 @@ class OptionsIPAddressACL(TypedDict, total=False): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, you have to independently monitor their relevance. + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ @@ -1548,6 +1549,9 @@ class OptionsUserAgentACL(TypedDict, total=False): - **allow** - List of User-Agents for which access is prohibited. - **deny** - List of User-Agents for which access is allowed. + You can provide exact User-Agent strings or regular expressions. Regular + expressions must start with `~` (case-sensitive) or `~*` (case-insensitive). + Use an empty string `""` to allow/deny access when the User-Agent header is empty. """ @@ -1773,8 +1777,9 @@ class Options(TypedDict, total=False): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, - you have to independently monitor their relevance. We recommend you use a script - for automatically update IP ACL. + you have to independently monitor their relevance. + + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ diff --git a/src/gcore/types/cdn/resources/rule_replace_params.py b/src/gcore/types/cdn/resources/rule_replace_params.py index 44a3a9f7..86fb2b30 100644 --- a/src/gcore/types/cdn/resources/rule_replace_params.py +++ b/src/gcore/types/cdn/resources/rule_replace_params.py @@ -840,6 +840,7 @@ class OptionsIPAddressACL(TypedDict, total=False): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, you have to independently monitor their relevance. + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ @@ -1550,6 +1551,9 @@ class OptionsUserAgentACL(TypedDict, total=False): - **allow** - List of User-Agents for which access is prohibited. - **deny** - List of User-Agents for which access is allowed. + You can provide exact User-Agent strings or regular expressions. Regular + expressions must start with `~` (case-sensitive) or `~*` (case-insensitive). + Use an empty string `""` to allow/deny access when the User-Agent header is empty. """ @@ -1775,8 +1779,9 @@ class Options(TypedDict, total=False): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, - you have to independently monitor their relevance. We recommend you use a script - for automatically update IP ACL. + you have to independently monitor their relevance. + + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ diff --git a/src/gcore/types/cdn/resources/rule_update_params.py b/src/gcore/types/cdn/resources/rule_update_params.py index a689c70f..c4ae3b10 100644 --- a/src/gcore/types/cdn/resources/rule_update_params.py +++ b/src/gcore/types/cdn/resources/rule_update_params.py @@ -840,6 +840,7 @@ class OptionsIPAddressACL(TypedDict, total=False): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, you have to independently monitor their relevance. + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ @@ -1550,6 +1551,9 @@ class OptionsUserAgentACL(TypedDict, total=False): - **allow** - List of User-Agents for which access is prohibited. - **deny** - List of User-Agents for which access is allowed. + You can provide exact User-Agent strings or regular expressions. Regular + expressions must start with `~` (case-sensitive) or `~*` (case-insensitive). + Use an empty string `""` to allow/deny access when the User-Agent header is empty. """ @@ -1775,8 +1779,9 @@ class Options(TypedDict, total=False): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, - you have to independently monitor their relevance. We recommend you use a script - for automatically update IP ACL. + you have to independently monitor their relevance. + + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ diff --git a/src/gcore/types/cdn/rule_template.py b/src/gcore/types/cdn/rule_template.py index 12360cca..e23d3805 100644 --- a/src/gcore/types/cdn/rule_template.py +++ b/src/gcore/types/cdn/rule_template.py @@ -762,6 +762,7 @@ class OptionsIPAddressACL(BaseModel): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, you have to independently monitor their relevance. + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ @@ -1474,6 +1475,9 @@ class OptionsUserAgentACL(BaseModel): - **allow** - List of User-Agents for which access is prohibited. - **deny** - List of User-Agents for which access is allowed. + You can provide exact User-Agent strings or regular expressions. Regular + expressions must start with `~` (case-sensitive) or `~*` (case-insensitive). + Use an empty string `""` to allow/deny access when the User-Agent header is empty. """ @@ -1699,8 +1703,9 @@ class Options(BaseModel): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, - you have to independently monitor their relevance. We recommend you use a script - for automatically update IP ACL. + you have to independently monitor their relevance. + + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ diff --git a/src/gcore/types/cdn/rule_template_create_params.py b/src/gcore/types/cdn/rule_template_create_params.py index 5a000d28..6d44873b 100644 --- a/src/gcore/types/cdn/rule_template_create_params.py +++ b/src/gcore/types/cdn/rule_template_create_params.py @@ -822,6 +822,7 @@ class OptionsIPAddressACL(TypedDict, total=False): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, you have to independently monitor their relevance. + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ @@ -1532,6 +1533,9 @@ class OptionsUserAgentACL(TypedDict, total=False): - **allow** - List of User-Agents for which access is prohibited. - **deny** - List of User-Agents for which access is allowed. + You can provide exact User-Agent strings or regular expressions. Regular + expressions must start with `~` (case-sensitive) or `~*` (case-insensitive). + Use an empty string `""` to allow/deny access when the User-Agent header is empty. """ @@ -1757,8 +1761,9 @@ class Options(TypedDict, total=False): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, - you have to independently monitor their relevance. We recommend you use a script - for automatically update IP ACL. + you have to independently monitor their relevance. + + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ diff --git a/src/gcore/types/cdn/rule_template_replace_params.py b/src/gcore/types/cdn/rule_template_replace_params.py index 4af0182e..7dba15b1 100644 --- a/src/gcore/types/cdn/rule_template_replace_params.py +++ b/src/gcore/types/cdn/rule_template_replace_params.py @@ -822,6 +822,7 @@ class OptionsIPAddressACL(TypedDict, total=False): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, you have to independently monitor their relevance. + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ @@ -1532,6 +1533,9 @@ class OptionsUserAgentACL(TypedDict, total=False): - **allow** - List of User-Agents for which access is prohibited. - **deny** - List of User-Agents for which access is allowed. + You can provide exact User-Agent strings or regular expressions. Regular + expressions must start with `~` (case-sensitive) or `~*` (case-insensitive). + Use an empty string `""` to allow/deny access when the User-Agent header is empty. """ @@ -1757,8 +1761,9 @@ class Options(TypedDict, total=False): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, - you have to independently monitor their relevance. We recommend you use a script - for automatically update IP ACL. + you have to independently monitor their relevance. + + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ diff --git a/src/gcore/types/cdn/rule_template_update_params.py b/src/gcore/types/cdn/rule_template_update_params.py index 8d45b593..37b2ec5b 100644 --- a/src/gcore/types/cdn/rule_template_update_params.py +++ b/src/gcore/types/cdn/rule_template_update_params.py @@ -822,6 +822,7 @@ class OptionsIPAddressACL(TypedDict, total=False): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, you have to independently monitor their relevance. + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ @@ -1532,6 +1533,9 @@ class OptionsUserAgentACL(TypedDict, total=False): - **allow** - List of User-Agents for which access is prohibited. - **deny** - List of User-Agents for which access is allowed. + You can provide exact User-Agent strings or regular expressions. Regular + expressions must start with `~` (case-sensitive) or `~*` (case-insensitive). + Use an empty string `""` to allow/deny access when the User-Agent header is empty. """ @@ -1757,8 +1761,9 @@ class Options(TypedDict, total=False): """Controls access to the CDN resource content for specific IP addresses. If you want to use IPs from our CDN servers IP list for IP ACL configuration, - you have to independently monitor their relevance. We recommend you use a script - for automatically update IP ACL. + you have to independently monitor their relevance. + + We recommend you use a script for automatically update IP ACL. [Read more.](/docs/api-reference/cdn/ip-addresses-list/get-cdn-servers-ip-addresses) """ diff --git a/tests/api_resources/cdn/resources/test_rules.py b/tests/api_resources/cdn/resources/test_rules.py index 63940c96..351ea91b 100644 --- a/tests/api_resources/cdn/resources/test_rules.py +++ b/tests/api_resources/cdn/resources/test_rules.py @@ -291,7 +291,7 @@ def test_method_create_with_all_params(self, client: Gcore) -> None: }, "user_agent_acl": { "enabled": True, - "excepted_values": ["UserAgent Value", ""], + "excepted_values": ["UserAgent Value", "~*.*bot.*", ""], "policy_type": "allow", }, "waap": { @@ -607,7 +607,7 @@ def test_method_update_with_all_params(self, client: Gcore) -> None: }, "user_agent_acl": { "enabled": True, - "excepted_values": ["UserAgent Value", ""], + "excepted_values": ["UserAgent Value", "~*.*bot.*", ""], "policy_type": "allow", }, "waap": { @@ -1024,7 +1024,7 @@ def test_method_replace_with_all_params(self, client: Gcore) -> None: }, "user_agent_acl": { "enabled": True, - "excepted_values": ["UserAgent Value", ""], + "excepted_values": ["UserAgent Value", "~*.*bot.*", ""], "policy_type": "allow", }, "waap": { @@ -1349,7 +1349,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncGcore) -> }, "user_agent_acl": { "enabled": True, - "excepted_values": ["UserAgent Value", ""], + "excepted_values": ["UserAgent Value", "~*.*bot.*", ""], "policy_type": "allow", }, "waap": { @@ -1665,7 +1665,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncGcore) -> }, "user_agent_acl": { "enabled": True, - "excepted_values": ["UserAgent Value", ""], + "excepted_values": ["UserAgent Value", "~*.*bot.*", ""], "policy_type": "allow", }, "waap": { @@ -2082,7 +2082,7 @@ async def test_method_replace_with_all_params(self, async_client: AsyncGcore) -> }, "user_agent_acl": { "enabled": True, - "excepted_values": ["UserAgent Value", ""], + "excepted_values": ["UserAgent Value", "~*.*bot.*", ""], "policy_type": "allow", }, "waap": { diff --git a/tests/api_resources/cdn/test_resources.py b/tests/api_resources/cdn/test_resources.py index debacd03..a987d85b 100644 --- a/tests/api_resources/cdn/test_resources.py +++ b/tests/api_resources/cdn/test_resources.py @@ -311,7 +311,7 @@ def test_method_create_with_all_params(self, client: Gcore) -> None: }, "user_agent_acl": { "enabled": True, - "excepted_values": ["UserAgent Value", ""], + "excepted_values": ["UserAgent Value", "~*.*bot.*", ""], "policy_type": "allow", }, "waap": { @@ -652,7 +652,7 @@ def test_method_update_with_all_params(self, client: Gcore) -> None: }, "user_agent_acl": { "enabled": True, - "excepted_values": ["UserAgent Value", ""], + "excepted_values": ["UserAgent Value", "~*.*bot.*", ""], "policy_type": "allow", }, "waap": { @@ -1281,7 +1281,7 @@ def test_method_replace_with_all_params(self, client: Gcore) -> None: }, "user_agent_acl": { "enabled": True, - "excepted_values": ["UserAgent Value", ""], + "excepted_values": ["UserAgent Value", "~*.*bot.*", ""], "policy_type": "allow", }, "waap": { @@ -1627,7 +1627,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncGcore) -> }, "user_agent_acl": { "enabled": True, - "excepted_values": ["UserAgent Value", ""], + "excepted_values": ["UserAgent Value", "~*.*bot.*", ""], "policy_type": "allow", }, "waap": { @@ -1968,7 +1968,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncGcore) -> }, "user_agent_acl": { "enabled": True, - "excepted_values": ["UserAgent Value", ""], + "excepted_values": ["UserAgent Value", "~*.*bot.*", ""], "policy_type": "allow", }, "waap": { @@ -2597,7 +2597,7 @@ async def test_method_replace_with_all_params(self, async_client: AsyncGcore) -> }, "user_agent_acl": { "enabled": True, - "excepted_values": ["UserAgent Value", ""], + "excepted_values": ["UserAgent Value", "~*.*bot.*", ""], "policy_type": "allow", }, "waap": { diff --git a/tests/api_resources/cdn/test_rule_templates.py b/tests/api_resources/cdn/test_rule_templates.py index cf4af42c..e7d8cb23 100644 --- a/tests/api_resources/cdn/test_rule_templates.py +++ b/tests/api_resources/cdn/test_rule_templates.py @@ -287,7 +287,7 @@ def test_method_create_with_all_params(self, client: Gcore) -> None: }, "user_agent_acl": { "enabled": True, - "excepted_values": ["UserAgent Value", ""], + "excepted_values": ["UserAgent Value", "~*.*bot.*", ""], "policy_type": "allow", }, "waap": { @@ -595,7 +595,7 @@ def test_method_update_with_all_params(self, client: Gcore) -> None: }, "user_agent_acl": { "enabled": True, - "excepted_values": ["UserAgent Value", ""], + "excepted_values": ["UserAgent Value", "~*.*bot.*", ""], "policy_type": "allow", }, "waap": { @@ -994,7 +994,7 @@ def test_method_replace_with_all_params(self, client: Gcore) -> None: }, "user_agent_acl": { "enabled": True, - "excepted_values": ["UserAgent Value", ""], + "excepted_values": ["UserAgent Value", "~*.*bot.*", ""], "policy_type": "allow", }, "waap": { @@ -1312,7 +1312,7 @@ async def test_method_create_with_all_params(self, async_client: AsyncGcore) -> }, "user_agent_acl": { "enabled": True, - "excepted_values": ["UserAgent Value", ""], + "excepted_values": ["UserAgent Value", "~*.*bot.*", ""], "policy_type": "allow", }, "waap": { @@ -1620,7 +1620,7 @@ async def test_method_update_with_all_params(self, async_client: AsyncGcore) -> }, "user_agent_acl": { "enabled": True, - "excepted_values": ["UserAgent Value", ""], + "excepted_values": ["UserAgent Value", "~*.*bot.*", ""], "policy_type": "allow", }, "waap": { @@ -2019,7 +2019,7 @@ async def test_method_replace_with_all_params(self, async_client: AsyncGcore) -> }, "user_agent_acl": { "enabled": True, - "excepted_values": ["UserAgent Value", ""], + "excepted_values": ["UserAgent Value", "~*.*bot.*", ""], "policy_type": "allow", }, "waap": { From f535e677d1fb2779103eac15f600557c1981184d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 07:37:18 +0000 Subject: [PATCH 04/12] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index cd2e413f..d9ef3c3d 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 645 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gcore%2Fgcore-dc42eef367287a7e7bcd1a562a519594fd3ac1b50ee86cfd8c6337b334dfbf25.yml openapi_spec_hash: e72c6e3d4a64c6fc34a6b73431e65fba -config_hash: e9e5b750687e9071d8b606963f0ffd6d +config_hash: 2f023b35fc459cc07b468f79c8c2864e From 46158578ca15288afaac9c08b927cb99e605cab5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 08:14:47 +0000 Subject: [PATCH 05/12] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index d9ef3c3d..e75e4409 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 645 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gcore%2Fgcore-dc42eef367287a7e7bcd1a562a519594fd3ac1b50ee86cfd8c6337b334dfbf25.yml -openapi_spec_hash: e72c6e3d4a64c6fc34a6b73431e65fba +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gcore%2Fgcore-e7e725cbd4047b87dac16a599ceaca2a7f59cc34401e203328bc5b50f7582a59.yml +openapi_spec_hash: 3056511ca272b6b265efdd4bc8e8c437 config_hash: 2f023b35fc459cc07b468f79c8c2864e From 25dad797438e97d8e501c139dd95c4abbb161781 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:51:05 +0000 Subject: [PATCH 06/12] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index e75e4409..4e8e1e6c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 645 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gcore%2Fgcore-e7e725cbd4047b87dac16a599ceaca2a7f59cc34401e203328bc5b50f7582a59.yml openapi_spec_hash: 3056511ca272b6b265efdd4bc8e8c437 -config_hash: 2f023b35fc459cc07b468f79c8c2864e +config_hash: 98f7a9b9463b6248408d48a4adbb14d3 From 47734d2b4fcd22875d2ec80b2b0453600b912eca Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 09:39:23 +0000 Subject: [PATCH 07/12] feat(api): manual upload of aggregated API specs --- .stats.yml | 4 +- src/gcore/resources/dns/metrics.py | 8 +- src/gcore/resources/dns/zones/rrsets.py | 240 +++++++----------- src/gcore/resources/streaming/playlists.py | 4 + src/gcore/types/streaming/playlist.py | 1 + .../types/streaming/playlist_create_params.py | 1 + .../types/streaming/playlist_update_params.py | 1 + src/gcore/types/streaming/video.py | 4 +- 8 files changed, 111 insertions(+), 152 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4e8e1e6c..839f1657 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 645 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gcore%2Fgcore-e7e725cbd4047b87dac16a599ceaca2a7f59cc34401e203328bc5b50f7582a59.yml -openapi_spec_hash: 3056511ca272b6b265efdd4bc8e8c437 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gcore%2Fgcore-f9e3248b7c9cc29017d94b17dd33fbc7b3eaa4eda92c1c7202b41b8cca84bc33.yml +openapi_spec_hash: cb53ad5e99f343ea9adc322d9f805b63 config_hash: 98f7a9b9463b6248408d48a4adbb14d3 diff --git a/src/gcore/resources/dns/metrics.py b/src/gcore/resources/dns/metrics.py index 3359b610..918583f4 100644 --- a/src/gcore/resources/dns/metrics.py +++ b/src/gcore/resources/dns/metrics.py @@ -58,8 +58,8 @@ def list( Example of success response: ``` - # HELP healthcheck_state The `healthcheck_state` metric reflects the state of a specific monitor after conducting a health check - # TYPE healthcheck_state gauge + HELP healthcheck_state The `healthcheck_state` metric reflects the state of a specific monitor after conducting a health check + TYPE healthcheck_state gauge healthcheck_state{client_id="1",monitor_id="431",monitor_locations="us-east-1,us-west-1",monitor_name="test-monitor-1",monitor_type="http",rrset_name="rrset-name1",rrset_type="rrset-type1",zone_name="zone-name1"} 0 healthcheck_state{client_id="1",monitor_id="4871",monitor_locations="fr-1,fr-2",monitor_name="test-monitor-2",monitor_type="tcp",rrset_name="rrset-name2",rrset_type="rrset-type2",zone_name="zone-name2"} 1 healthcheck_state{client_id="2",monitor_id="7123",monitor_locations="ua-1,ua-2",monitor_name="test-monitor-3",monitor_type="icmp",rrset_name="rrset-name3",rrset_type="rrset-type3",zone_name="zone-name3"} 0 @@ -136,8 +136,8 @@ async def list( Example of success response: ``` - # HELP healthcheck_state The `healthcheck_state` metric reflects the state of a specific monitor after conducting a health check - # TYPE healthcheck_state gauge + HELP healthcheck_state The `healthcheck_state` metric reflects the state of a specific monitor after conducting a health check + TYPE healthcheck_state gauge healthcheck_state{client_id="1",monitor_id="431",monitor_locations="us-east-1,us-west-1",monitor_name="test-monitor-1",monitor_type="http",rrset_name="rrset-name1",rrset_type="rrset-type1",zone_name="zone-name1"} 0 healthcheck_state{client_id="1",monitor_id="4871",monitor_locations="fr-1,fr-2",monitor_name="test-monitor-2",monitor_type="tcp",rrset_name="rrset-name2",rrset_type="rrset-type2",zone_name="zone-name2"} 1 healthcheck_state{client_id="2",monitor_id="7123",monitor_locations="ua-1,ua-2",monitor_name="test-monitor-3",monitor_type="icmp",rrset_name="rrset-name3",rrset_type="rrset-type3",zone_name="zone-name3"} 0 diff --git a/src/gcore/resources/dns/zones/rrsets.py b/src/gcore/resources/dns/zones/rrsets.py index b1ab12a5..736b590d 100644 --- a/src/gcore/resources/dns/zones/rrsets.py +++ b/src/gcore/resources/dns/zones/rrsets.py @@ -72,32 +72,27 @@ def create( Add the RRSet to the zone specified by zoneName, RRSets can be configured to be either dynamic or static. - ### Static RRsets - - Staticly configured RRSets provide DNS responses as is. - - ### Dynamic RRsets - - Dynamic RRSets have picker configuration defined thus it's possible to finely - customize DNS response. Picking rules are defined on the RRSet level as a list - of selectors, filters and mutators. Picker considers different resource records - metadata, requestor IP, and other event-feeds like monitoring. Picker - configuration is an ordered list defined by "pickers" attribute. Requestor IP is - determined by EDNS Client Subnet (ECS) if defined, otherwise - by - client/recursor IP. Selector pickers are used in the specified order until the - first match, in case of match - all next selectors are bypassed. Filters or - mutators are applied to the match according to the order they are specified. + Static RRsets Staticly configured RRSets provide DNS responses as is. + + Dynamic RRsets Dynamic RRSets have picker configuration defined thus it's + possible to finely customize DNS response. Picking rules are defined on the + RRSet level as a list of selectors, filters and mutators. Picker considers + different resource records metadata, requestor IP, and other event-feeds like + monitoring. Picker configuration is an ordered list defined by "pickers" + attribute. Requestor IP is determined by EDNS Client Subnet (ECS) if defined, + otherwise - by client/recursor IP. Selector pickers are used in the specified + order until the first match, in case of match - all next selectors are bypassed. + Filters or mutators are applied to the match according to the order they are + specified. For example, sort records by proximity to user, shuffle based on weights and return not more than 3: `"pickers": [ { "type": "geodistance" }, { "type": "weighted_shuffle" }, { "type": "first_n", "limit": 3 } ]` - #### geodns filter - - A resource record is included in the answer if resource record's metadata - matches requestor info. For each resource record in RRSet, the following - metadata is considered (in the order specified): + geodns filter A resource record is included in the answer if resource record's + metadata matches requestor info. For each resource record in RRSet, the + following metadata is considered (in the order specified): - `ip` - list of network addresses in CIDR format, e.g. `["192.168.15.150/25", "2003:de:2016::/48"]`; @@ -114,91 +109,71 @@ def create( Example: `"pickers": [ { "type": "geodns", "strict": true } ]` - ##### Strict parameter - - `strict: true` means that if no records percolate through the geodns filter it - returns no answers. `strict: false` means that if no records percolate through - the geodns filter, all records are passed over. + Strict parameter `strict: true` means that if no records percolate through the + geodns filter it returns no answers. `strict: false` means that if no records + percolate through the geodns filter, all records are passed over. - #### asn selector - - Resource records which ASN metadata matches ASN of the requestor are picked by - this selector, and passed to the next non-selector picker, if there is no - match - next configured picker starts with all records. + asn selector Resource records which ASN metadata matches ASN of the requestor + are picked by this selector, and passed to the next non-selector picker, if + there is no match - next configured picker starts with all records. Example: `"pickers": [ {"type": "asn"} ]` - #### country selector - - Resource records which country metadata matches country of the requestor are - picked by this selector, and passed to the next non-selector picker, if there is - no match - next configured picker starts with all records. + country selector Resource records which country metadata matches country of the + requestor are picked by this selector, and passed to the next non-selector + picker, if there is no match - next configured picker starts with all records. Example: `"pickers": [ { "type": "country" } ]` - #### continent selector - - Resource records which continent metadata matches continent of the requestor are - picked by this selector, and passed to the next non-selector picker, if there is - no match - next configured picker starts with all records. + continent selector Resource records which continent metadata matches continent + of the requestor are picked by this selector, and passed to the next + non-selector picker, if there is no match - next configured picker starts with + all records. Example: `"pickers": [ { "type": "continent" } ]` - #### region selector - - Resource records which region metadata matches region of the requestor are - picked by this selector, and passed to the next non-selector picker, if there is - no match - next configured picker starts with all records. e.g. `fr-nor` for - France/Normandy. + region selector Resource records which region metadata matches region of the + requestor are picked by this selector, and passed to the next non-selector + picker, if there is no match - next configured picker starts with all records. + e.g. `fr-nor` for France/Normandy. Example: `"pickers": [ { "type": "region" } ]` - #### ip selector - - Resource records which IP metadata matches IP of the requestor are picked by - this selector, and passed to the next non-selector picker, if there is no - match - next configured picker starts with all records. Maximum 100 subnets are - allowed to specify in meta of RR. + ip selector Resource records which IP metadata matches IP of the requestor are + picked by this selector, and passed to the next non-selector picker, if there is + no match - next configured picker starts with all records. Maximum 100 subnets + are allowed to specify in meta of RR. Example: `"pickers": [ { "type": "ip" } ]` - #### default selector - - When enabled, records marked as default are selected: + default selector When enabled, records marked as default are selected: `"meta": {"default": true}`. Example: `"pickers": [ { "type": "geodns", "strict": false }, { "type": "default" }, { "type": "first_n", "limit": 2 } ]` - #### geodistance mutator - - The resource records are rearranged in ascending order based on the distance (in - meters) from requestor to the coordinates specified in latlong metadata. - Distance is calculated using Haversine formula. The "nearest" to the user's IP - RR goes first. The records without latlong metadata come last. e.g. for Berlin - `[52.520008, 13.404954]`.; + geodistance mutator The resource records are rearranged in ascending order based + on the distance (in meters) from requestor to the coordinates specified in + latlong metadata. Distance is calculated using Haversine formula. The "nearest" + to the user's IP RR goes first. The records without latlong metadata come last. + e.g. for Berlin `[52.520008, 13.404954]`.; In this configuration the only "nearest" to the requestor record to be returned: `"pickers": [ { "type": "geodistance" }, { "type": "first_n", "limit": 1 } ]` - #### `weighted_shuffle` mutator - - The resource records are rearranged in random order based on the `weight` - metadata. Default weight (if not specified) is 50. + `weighted_shuffle` mutator The resource records are rearranged in random order + based on the `weight` metadata. Default weight (if not specified) is 50. Example: `"pickers": [ { "type": "weighted_shuffle" } ]` - #### `first_n` filter - - Slices first N (N specified as a limit parameter value) resource records. + `first_n` filter Slices first N (N specified as a limit parameter value) + resource records. Example: `"pickers": [ { "type": "first_n", "limit": 1 } ]` returns only the first resource record. - ##### limit parameter - - Can be a positive value for a specific limit. Use zero or leave it blank to - indicate no limits. + limit parameter Can be a positive value for a specific limit. Use zero or leave + it blank to indicate no limits. Args: resource_records: List of resource record from rrset @@ -528,32 +503,27 @@ async def create( Add the RRSet to the zone specified by zoneName, RRSets can be configured to be either dynamic or static. - ### Static RRsets + Static RRsets Staticly configured RRSets provide DNS responses as is. - Staticly configured RRSets provide DNS responses as is. - - ### Dynamic RRsets - - Dynamic RRSets have picker configuration defined thus it's possible to finely - customize DNS response. Picking rules are defined on the RRSet level as a list - of selectors, filters and mutators. Picker considers different resource records - metadata, requestor IP, and other event-feeds like monitoring. Picker - configuration is an ordered list defined by "pickers" attribute. Requestor IP is - determined by EDNS Client Subnet (ECS) if defined, otherwise - by - client/recursor IP. Selector pickers are used in the specified order until the - first match, in case of match - all next selectors are bypassed. Filters or - mutators are applied to the match according to the order they are specified. + Dynamic RRsets Dynamic RRSets have picker configuration defined thus it's + possible to finely customize DNS response. Picking rules are defined on the + RRSet level as a list of selectors, filters and mutators. Picker considers + different resource records metadata, requestor IP, and other event-feeds like + monitoring. Picker configuration is an ordered list defined by "pickers" + attribute. Requestor IP is determined by EDNS Client Subnet (ECS) if defined, + otherwise - by client/recursor IP. Selector pickers are used in the specified + order until the first match, in case of match - all next selectors are bypassed. + Filters or mutators are applied to the match according to the order they are + specified. For example, sort records by proximity to user, shuffle based on weights and return not more than 3: `"pickers": [ { "type": "geodistance" }, { "type": "weighted_shuffle" }, { "type": "first_n", "limit": 3 } ]` - #### geodns filter - - A resource record is included in the answer if resource record's metadata - matches requestor info. For each resource record in RRSet, the following - metadata is considered (in the order specified): + geodns filter A resource record is included in the answer if resource record's + metadata matches requestor info. For each resource record in RRSet, the + following metadata is considered (in the order specified): - `ip` - list of network addresses in CIDR format, e.g. `["192.168.15.150/25", "2003:de:2016::/48"]`; @@ -570,91 +540,71 @@ async def create( Example: `"pickers": [ { "type": "geodns", "strict": true } ]` - ##### Strict parameter - - `strict: true` means that if no records percolate through the geodns filter it - returns no answers. `strict: false` means that if no records percolate through - the geodns filter, all records are passed over. - - #### asn selector + Strict parameter `strict: true` means that if no records percolate through the + geodns filter it returns no answers. `strict: false` means that if no records + percolate through the geodns filter, all records are passed over. - Resource records which ASN metadata matches ASN of the requestor are picked by - this selector, and passed to the next non-selector picker, if there is no - match - next configured picker starts with all records. + asn selector Resource records which ASN metadata matches ASN of the requestor + are picked by this selector, and passed to the next non-selector picker, if + there is no match - next configured picker starts with all records. Example: `"pickers": [ {"type": "asn"} ]` - #### country selector - - Resource records which country metadata matches country of the requestor are - picked by this selector, and passed to the next non-selector picker, if there is - no match - next configured picker starts with all records. + country selector Resource records which country metadata matches country of the + requestor are picked by this selector, and passed to the next non-selector + picker, if there is no match - next configured picker starts with all records. Example: `"pickers": [ { "type": "country" } ]` - #### continent selector - - Resource records which continent metadata matches continent of the requestor are - picked by this selector, and passed to the next non-selector picker, if there is - no match - next configured picker starts with all records. + continent selector Resource records which continent metadata matches continent + of the requestor are picked by this selector, and passed to the next + non-selector picker, if there is no match - next configured picker starts with + all records. Example: `"pickers": [ { "type": "continent" } ]` - #### region selector - - Resource records which region metadata matches region of the requestor are - picked by this selector, and passed to the next non-selector picker, if there is - no match - next configured picker starts with all records. e.g. `fr-nor` for - France/Normandy. + region selector Resource records which region metadata matches region of the + requestor are picked by this selector, and passed to the next non-selector + picker, if there is no match - next configured picker starts with all records. + e.g. `fr-nor` for France/Normandy. Example: `"pickers": [ { "type": "region" } ]` - #### ip selector - - Resource records which IP metadata matches IP of the requestor are picked by - this selector, and passed to the next non-selector picker, if there is no - match - next configured picker starts with all records. Maximum 100 subnets are - allowed to specify in meta of RR. + ip selector Resource records which IP metadata matches IP of the requestor are + picked by this selector, and passed to the next non-selector picker, if there is + no match - next configured picker starts with all records. Maximum 100 subnets + are allowed to specify in meta of RR. Example: `"pickers": [ { "type": "ip" } ]` - #### default selector - - When enabled, records marked as default are selected: + default selector When enabled, records marked as default are selected: `"meta": {"default": true}`. Example: `"pickers": [ { "type": "geodns", "strict": false }, { "type": "default" }, { "type": "first_n", "limit": 2 } ]` - #### geodistance mutator - - The resource records are rearranged in ascending order based on the distance (in - meters) from requestor to the coordinates specified in latlong metadata. - Distance is calculated using Haversine formula. The "nearest" to the user's IP - RR goes first. The records without latlong metadata come last. e.g. for Berlin - `[52.520008, 13.404954]`.; + geodistance mutator The resource records are rearranged in ascending order based + on the distance (in meters) from requestor to the coordinates specified in + latlong metadata. Distance is calculated using Haversine formula. The "nearest" + to the user's IP RR goes first. The records without latlong metadata come last. + e.g. for Berlin `[52.520008, 13.404954]`.; In this configuration the only "nearest" to the requestor record to be returned: `"pickers": [ { "type": "geodistance" }, { "type": "first_n", "limit": 1 } ]` - #### `weighted_shuffle` mutator - - The resource records are rearranged in random order based on the `weight` - metadata. Default weight (if not specified) is 50. + `weighted_shuffle` mutator The resource records are rearranged in random order + based on the `weight` metadata. Default weight (if not specified) is 50. Example: `"pickers": [ { "type": "weighted_shuffle" } ]` - #### `first_n` filter - - Slices first N (N specified as a limit parameter value) resource records. + `first_n` filter Slices first N (N specified as a limit parameter value) + resource records. Example: `"pickers": [ { "type": "first_n", "limit": 1 } ]` returns only the first resource record. - ##### limit parameter - - Can be a positive value for a specific limit. Use zero or leave it blank to - indicate no limits. + limit parameter Can be a positive value for a specific limit. Use zero or leave + it blank to indicate no limits. Args: resource_records: List of resource record from rrset diff --git a/src/gcore/resources/streaming/playlists.py b/src/gcore/resources/streaming/playlists.py index fe1f49db..28f15c1a 100644 --- a/src/gcore/resources/streaming/playlists.py +++ b/src/gcore/resources/streaming/playlists.py @@ -173,6 +173,7 @@ def create( This URL is a link to the main manifest. But you can also manually specify suffix-options that will allow you to change the manifest to your request: + `/playlists/{client_id}_{playlist_id}/master[-cmaf][-min-N][-max-N][-img][-(h264|hevc|av1)].m3u8` Please see the details in `hls_url` attribute of /videos/{id} method. @@ -308,6 +309,7 @@ def update( This URL is a link to the main manifest. But you can also manually specify suffix-options that will allow you to change the manifest to your request: + `/playlists/{client_id}_{playlist_id}/master[-cmaf][-min-N][-max-N][-img][-(h264|hevc|av1)].m3u8` Please see the details in `hls_url` attribute of /videos/{id} method. @@ -662,6 +664,7 @@ async def create( This URL is a link to the main manifest. But you can also manually specify suffix-options that will allow you to change the manifest to your request: + `/playlists/{client_id}_{playlist_id}/master[-cmaf][-min-N][-max-N][-img][-(h264|hevc|av1)].m3u8` Please see the details in `hls_url` attribute of /videos/{id} method. @@ -797,6 +800,7 @@ async def update( This URL is a link to the main manifest. But you can also manually specify suffix-options that will allow you to change the manifest to your request: + `/playlists/{client_id}_{playlist_id}/master[-cmaf][-min-N][-max-N][-img][-(h264|hevc|av1)].m3u8` Please see the details in `hls_url` attribute of /videos/{id} method. diff --git a/src/gcore/types/streaming/playlist.py b/src/gcore/types/streaming/playlist.py index 495c206d..e0f94e66 100644 --- a/src/gcore/types/streaming/playlist.py +++ b/src/gcore/types/streaming/playlist.py @@ -49,6 +49,7 @@ class Playlist(BaseModel): This URL is a link to the main manifest. But you can also manually specify suffix-options that will allow you to change the manifest to your request: + `/playlists/{client_id}_{playlist_id}/master[-cmaf][-min-N][-max-N][-img][-(h264|hevc|av1)].m3u8` Please see the details in `hls_url` attribute of /videos/{id} method. diff --git a/src/gcore/types/streaming/playlist_create_params.py b/src/gcore/types/streaming/playlist_create_params.py index f9102a3f..2eebe230 100644 --- a/src/gcore/types/streaming/playlist_create_params.py +++ b/src/gcore/types/streaming/playlist_create_params.py @@ -49,6 +49,7 @@ class PlaylistCreateParams(TypedDict, total=False): This URL is a link to the main manifest. But you can also manually specify suffix-options that will allow you to change the manifest to your request: + `/playlists/{client_id}_{playlist_id}/master[-cmaf][-min-N][-max-N][-img][-(h264|hevc|av1)].m3u8` Please see the details in `hls_url` attribute of /videos/{id} method. diff --git a/src/gcore/types/streaming/playlist_update_params.py b/src/gcore/types/streaming/playlist_update_params.py index 10fe4362..ce411690 100644 --- a/src/gcore/types/streaming/playlist_update_params.py +++ b/src/gcore/types/streaming/playlist_update_params.py @@ -49,6 +49,7 @@ class PlaylistUpdateParams(TypedDict, total=False): This URL is a link to the main manifest. But you can also manually specify suffix-options that will allow you to change the manifest to your request: + `/playlists/{client_id}_{playlist_id}/master[-cmaf][-min-N][-max-N][-img][-(h264|hevc|av1)].m3u8` Please see the details in `hls_url` attribute of /videos/{id} method. diff --git a/src/gcore/types/streaming/video.py b/src/gcore/types/streaming/video.py index c12c6453..f6a405ee 100644 --- a/src/gcore/types/streaming/video.py +++ b/src/gcore/types/streaming/video.py @@ -85,7 +85,9 @@ class ConvertedVideo(BaseModel): **Default MP4 file name structure** Link to the file {filename} contains information about the encoding method using - format: `___.mp4` + format: + + `___.mp4` - `` – Internal quality identifier and file version. Please do not use it, can be changed at any time without any notice. From cd35cbf266cf598045a8a4f8ebf3cf7cd8340092 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:17:20 +0000 Subject: [PATCH 08/12] feat(api): aggregated API specs update --- .stats.yml | 4 +- .../resources/waap/domains/statistics.py | 45 ++++-- .../statistic_get_events_aggregated_params.py | 2 +- .../waap/domains/test_statistics.py | 142 ++++++++++-------- 4 files changed, 112 insertions(+), 81 deletions(-) diff --git a/.stats.yml b/.stats.yml index 839f1657..b1d69914 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 645 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gcore%2Fgcore-f9e3248b7c9cc29017d94b17dd33fbc7b3eaa4eda92c1c7202b41b8cca84bc33.yml -openapi_spec_hash: cb53ad5e99f343ea9adc322d9f805b63 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gcore%2Fgcore-265fa735ac3d620de0e726578c506cd13b38ca938cc853de65c5d65d8c831124.yml +openapi_spec_hash: 3641df0a7f7ebdb7dd55d0fa377251d8 config_hash: 98f7a9b9463b6248408d48a4adbb14d3 diff --git a/src/gcore/resources/waap/domains/statistics.py b/src/gcore/resources/waap/domains/statistics.py index 94514578..225b0f77 100644 --- a/src/gcore/resources/waap/domains/statistics.py +++ b/src/gcore/resources/waap/domains/statistics.py @@ -2,6 +2,7 @@ from __future__ import annotations +import typing_extensions from typing import List, Union, Optional from datetime import datetime from typing_extensions import Literal @@ -187,7 +188,7 @@ def get_events_aggregated( domain_id: int, *, start: str, - action: Optional[List[Literal["block", "captcha", "handshake", "monitor"]]] | Omit = omit, + action: Optional[List[Literal["allow", "block", "captcha", "handshake"]]] | Omit = omit, end: Optional[str] | Omit = omit, ip: Optional[SequenceNotStr[str]] | Omit = omit, reference_id: Optional[SequenceNotStr[str]] | Omit = omit, @@ -287,6 +288,7 @@ def get_request_details( cast_to=WaapRequestDetails, ) + @typing_extensions.deprecated("deprecated") def get_requests_series( self, domain_id: int, @@ -334,8 +336,11 @@ def get_requests_series( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> SyncOffsetPage[WaapRequestSummary]: - """ - Retrieve a domain's requests data. + """Retrieve a domain's requests data. + + Deprecated. Use + [GET /v1/analytics/requests](/docs/api-reference/waap/analytics/get-request-log-data) + instead. Args: domain_id: The domain ID @@ -609,7 +614,7 @@ async def get_events_aggregated( domain_id: int, *, start: str, - action: Optional[List[Literal["block", "captcha", "handshake", "monitor"]]] | Omit = omit, + action: Optional[List[Literal["allow", "block", "captcha", "handshake"]]] | Omit = omit, end: Optional[str] | Omit = omit, ip: Optional[SequenceNotStr[str]] | Omit = omit, reference_id: Optional[SequenceNotStr[str]] | Omit = omit, @@ -709,6 +714,7 @@ async def get_request_details( cast_to=WaapRequestDetails, ) + @typing_extensions.deprecated("deprecated") def get_requests_series( self, domain_id: int, @@ -756,8 +762,11 @@ def get_requests_series( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncPaginator[WaapRequestSummary, AsyncOffsetPage[WaapRequestSummary]]: - """ - Retrieve a domain's requests data. + """Retrieve a domain's requests data. + + Deprecated. Use + [GET /v1/analytics/requests](/docs/api-reference/waap/analytics/get-request-log-data) + instead. Args: domain_id: The domain ID @@ -897,8 +906,10 @@ def __init__(self, statistics: StatisticsResource) -> None: self.get_request_details = to_raw_response_wrapper( statistics.get_request_details, ) - self.get_requests_series = to_raw_response_wrapper( - statistics.get_requests_series, + self.get_requests_series = ( # pyright: ignore[reportDeprecated] + to_raw_response_wrapper( + statistics.get_requests_series, # pyright: ignore[reportDeprecated], + ) ) self.get_traffic_series = to_raw_response_wrapper( statistics.get_traffic_series, @@ -921,8 +932,10 @@ def __init__(self, statistics: AsyncStatisticsResource) -> None: self.get_request_details = async_to_raw_response_wrapper( statistics.get_request_details, ) - self.get_requests_series = async_to_raw_response_wrapper( - statistics.get_requests_series, + self.get_requests_series = ( # pyright: ignore[reportDeprecated] + async_to_raw_response_wrapper( + statistics.get_requests_series, # pyright: ignore[reportDeprecated], + ) ) self.get_traffic_series = async_to_raw_response_wrapper( statistics.get_traffic_series, @@ -945,8 +958,10 @@ def __init__(self, statistics: StatisticsResource) -> None: self.get_request_details = to_streamed_response_wrapper( statistics.get_request_details, ) - self.get_requests_series = to_streamed_response_wrapper( - statistics.get_requests_series, + self.get_requests_series = ( # pyright: ignore[reportDeprecated] + to_streamed_response_wrapper( + statistics.get_requests_series, # pyright: ignore[reportDeprecated], + ) ) self.get_traffic_series = to_streamed_response_wrapper( statistics.get_traffic_series, @@ -969,8 +984,10 @@ def __init__(self, statistics: AsyncStatisticsResource) -> None: self.get_request_details = async_to_streamed_response_wrapper( statistics.get_request_details, ) - self.get_requests_series = async_to_streamed_response_wrapper( - statistics.get_requests_series, + self.get_requests_series = ( # pyright: ignore[reportDeprecated] + async_to_streamed_response_wrapper( + statistics.get_requests_series, # pyright: ignore[reportDeprecated], + ) ) self.get_traffic_series = async_to_streamed_response_wrapper( statistics.get_traffic_series, diff --git a/src/gcore/types/waap/domains/statistic_get_events_aggregated_params.py b/src/gcore/types/waap/domains/statistic_get_events_aggregated_params.py index ff19e38c..4cfc9bcd 100644 --- a/src/gcore/types/waap/domains/statistic_get_events_aggregated_params.py +++ b/src/gcore/types/waap/domains/statistic_get_events_aggregated_params.py @@ -14,7 +14,7 @@ class StatisticGetEventsAggregatedParams(TypedDict, total=False): start: Required[str] """Filter data items starting from a specified date in ISO 8601 format""" - action: Optional[List[Literal["block", "captcha", "handshake", "monitor"]]] + action: Optional[List[Literal["allow", "block", "captcha", "handshake"]]] """A list of action names to filter on.""" end: Optional[str] diff --git a/tests/api_resources/waap/domains/test_statistics.py b/tests/api_resources/waap/domains/test_statistics.py index f46a30a2..c8a4bcb4 100644 --- a/tests/api_resources/waap/domains/test_statistics.py +++ b/tests/api_resources/waap/domains/test_statistics.py @@ -20,6 +20,8 @@ StatisticGetTrafficSeriesResponse, ) +# pyright: reportDeprecated=false + base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") @@ -131,7 +133,7 @@ def test_method_get_events_aggregated_with_all_params(self, client: Gcore) -> No statistic = client.waap.domains.statistics.get_events_aggregated( domain_id=1, start="2024-04-13T00:00:00+01:00", - action=["block", "captcha"], + action=["allow", "block"], end="2024-04-14T12:00:00Z", ip=["string", "string"], reference_id=["string", "string"], @@ -209,37 +211,42 @@ def test_path_params_get_request_details(self, client: Gcore) -> None: @parametrize def test_method_get_requests_series(self, client: Gcore) -> None: - statistic = client.waap.domains.statistics.get_requests_series( - domain_id=1, - start="2024-04-13T00:00:00+01:00", - ) + with pytest.warns(DeprecationWarning): + statistic = client.waap.domains.statistics.get_requests_series( + domain_id=1, + start="2024-04-13T00:00:00+01:00", + ) + assert_matches_type(SyncOffsetPage[WaapRequestSummary], statistic, path=["response"]) @parametrize def test_method_get_requests_series_with_all_params(self, client: Gcore) -> None: - statistic = client.waap.domains.statistics.get_requests_series( - domain_id=1, - start="2024-04-13T00:00:00+01:00", - actions=["allow"], - countries=["Mv"], - end="2024-04-14T12:00:00Z", - ip=".:", - limit=0, - offset=0, - ordering="ordering", - reference_id="ad07c06f19054e484974fa22e9fb6bb1", - security_rule_name="security_rule_name", - status_code=100, - traffic_types=["policy_allowed"], - ) + with pytest.warns(DeprecationWarning): + statistic = client.waap.domains.statistics.get_requests_series( + domain_id=1, + start="2024-04-13T00:00:00+01:00", + actions=["allow"], + countries=["Mv"], + end="2024-04-14T12:00:00Z", + ip=".:", + limit=0, + offset=0, + ordering="ordering", + reference_id="ad07c06f19054e484974fa22e9fb6bb1", + security_rule_name="security_rule_name", + status_code=100, + traffic_types=["policy_allowed"], + ) + assert_matches_type(SyncOffsetPage[WaapRequestSummary], statistic, path=["response"]) @parametrize def test_raw_response_get_requests_series(self, client: Gcore) -> None: - response = client.waap.domains.statistics.with_raw_response.get_requests_series( - domain_id=1, - start="2024-04-13T00:00:00+01:00", - ) + with pytest.warns(DeprecationWarning): + response = client.waap.domains.statistics.with_raw_response.get_requests_series( + domain_id=1, + start="2024-04-13T00:00:00+01:00", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -248,15 +255,16 @@ def test_raw_response_get_requests_series(self, client: Gcore) -> None: @parametrize def test_streaming_response_get_requests_series(self, client: Gcore) -> None: - with client.waap.domains.statistics.with_streaming_response.get_requests_series( - domain_id=1, - start="2024-04-13T00:00:00+01:00", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + with client.waap.domains.statistics.with_streaming_response.get_requests_series( + domain_id=1, + start="2024-04-13T00:00:00+01:00", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - statistic = response.parse() - assert_matches_type(SyncOffsetPage[WaapRequestSummary], statistic, path=["response"]) + statistic = response.parse() + assert_matches_type(SyncOffsetPage[WaapRequestSummary], statistic, path=["response"]) assert cast(Any, response.is_closed) is True @@ -418,7 +426,7 @@ async def test_method_get_events_aggregated_with_all_params(self, async_client: statistic = await async_client.waap.domains.statistics.get_events_aggregated( domain_id=1, start="2024-04-13T00:00:00+01:00", - action=["block", "captcha"], + action=["allow", "block"], end="2024-04-14T12:00:00Z", ip=["string", "string"], reference_id=["string", "string"], @@ -496,37 +504,42 @@ async def test_path_params_get_request_details(self, async_client: AsyncGcore) - @parametrize async def test_method_get_requests_series(self, async_client: AsyncGcore) -> None: - statistic = await async_client.waap.domains.statistics.get_requests_series( - domain_id=1, - start="2024-04-13T00:00:00+01:00", - ) + with pytest.warns(DeprecationWarning): + statistic = await async_client.waap.domains.statistics.get_requests_series( + domain_id=1, + start="2024-04-13T00:00:00+01:00", + ) + assert_matches_type(AsyncOffsetPage[WaapRequestSummary], statistic, path=["response"]) @parametrize async def test_method_get_requests_series_with_all_params(self, async_client: AsyncGcore) -> None: - statistic = await async_client.waap.domains.statistics.get_requests_series( - domain_id=1, - start="2024-04-13T00:00:00+01:00", - actions=["allow"], - countries=["Mv"], - end="2024-04-14T12:00:00Z", - ip=".:", - limit=0, - offset=0, - ordering="ordering", - reference_id="ad07c06f19054e484974fa22e9fb6bb1", - security_rule_name="security_rule_name", - status_code=100, - traffic_types=["policy_allowed"], - ) + with pytest.warns(DeprecationWarning): + statistic = await async_client.waap.domains.statistics.get_requests_series( + domain_id=1, + start="2024-04-13T00:00:00+01:00", + actions=["allow"], + countries=["Mv"], + end="2024-04-14T12:00:00Z", + ip=".:", + limit=0, + offset=0, + ordering="ordering", + reference_id="ad07c06f19054e484974fa22e9fb6bb1", + security_rule_name="security_rule_name", + status_code=100, + traffic_types=["policy_allowed"], + ) + assert_matches_type(AsyncOffsetPage[WaapRequestSummary], statistic, path=["response"]) @parametrize async def test_raw_response_get_requests_series(self, async_client: AsyncGcore) -> None: - response = await async_client.waap.domains.statistics.with_raw_response.get_requests_series( - domain_id=1, - start="2024-04-13T00:00:00+01:00", - ) + with pytest.warns(DeprecationWarning): + response = await async_client.waap.domains.statistics.with_raw_response.get_requests_series( + domain_id=1, + start="2024-04-13T00:00:00+01:00", + ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -535,15 +548,16 @@ async def test_raw_response_get_requests_series(self, async_client: AsyncGcore) @parametrize async def test_streaming_response_get_requests_series(self, async_client: AsyncGcore) -> None: - async with async_client.waap.domains.statistics.with_streaming_response.get_requests_series( - domain_id=1, - start="2024-04-13T00:00:00+01:00", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" + with pytest.warns(DeprecationWarning): + async with async_client.waap.domains.statistics.with_streaming_response.get_requests_series( + domain_id=1, + start="2024-04-13T00:00:00+01:00", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" - statistic = await response.parse() - assert_matches_type(AsyncOffsetPage[WaapRequestSummary], statistic, path=["response"]) + statistic = await response.parse() + assert_matches_type(AsyncOffsetPage[WaapRequestSummary], statistic, path=["response"]) assert cast(Any, response.is_closed) is True From 3e060ccfc0b858af142d34a8b8759cd8d82f352d Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 06:25:06 +0000 Subject: [PATCH 09/12] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index b1d69914..925a1464 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 645 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gcore%2Fgcore-265fa735ac3d620de0e726578c506cd13b38ca938cc853de65c5d65d8c831124.yml -openapi_spec_hash: 3641df0a7f7ebdb7dd55d0fa377251d8 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gcore%2Fgcore-8b9ce3f2908dfcd3dab77ef7d831b407620420d7caef9551932b6789ec589bcc.yml +openapi_spec_hash: 15f1c6c29ccbc7120648e0e7c6476556 config_hash: 98f7a9b9463b6248408d48a4adbb14d3 From b883072e939928e5505c254524b24553310172dd Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 10:20:22 +0000 Subject: [PATCH 10/12] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index 925a1464..74fb9b3e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 645 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gcore%2Fgcore-8b9ce3f2908dfcd3dab77ef7d831b407620420d7caef9551932b6789ec589bcc.yml -openapi_spec_hash: 15f1c6c29ccbc7120648e0e7c6476556 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/gcore%2Fgcore-58d9afa7f8342ead022bd8fa12bb8abbeb9c0fb1e16f052ee6c4a59fae373e27.yml +openapi_spec_hash: 2ae4db03cfc907be71d44288503838d7 config_hash: 98f7a9b9463b6248408d48a4adbb14d3 From b0c58f9f042c3e67b63d99bded8179c943d29711 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:56:51 +0000 Subject: [PATCH 11/12] feat(client): add custom JSON encoder for extended type support --- src/gcore/_base_client.py | 7 +- src/gcore/_compat.py | 6 +- src/gcore/_utils/_json.py | 35 ++++++++++ tests/test_utils/test_json.py | 126 ++++++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 src/gcore/_utils/_json.py create mode 100644 tests/test_utils/test_json.py diff --git a/src/gcore/_base_client.py b/src/gcore/_base_client.py index e4303b57..e797b152 100644 --- a/src/gcore/_base_client.py +++ b/src/gcore/_base_client.py @@ -86,6 +86,7 @@ APIConnectionError, APIResponseValidationError, ) +from ._utils._json import openapi_dumps log: logging.Logger = logging.getLogger(__name__) @@ -554,8 +555,10 @@ def _build_request( kwargs["content"] = options.content elif isinstance(json_data, bytes): kwargs["content"] = json_data - else: - kwargs["json"] = json_data if is_given(json_data) else None + elif not files: + # Don't set content when JSON is sent as multipart/form-data, + # since httpx's content param overrides other body arguments + kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None kwargs["files"] = files else: headers.pop("Content-Type", None) diff --git a/src/gcore/_compat.py b/src/gcore/_compat.py index bdef67f0..786ff42a 100644 --- a/src/gcore/_compat.py +++ b/src/gcore/_compat.py @@ -139,6 +139,7 @@ def model_dump( exclude_defaults: bool = False, warnings: bool = True, mode: Literal["json", "python"] = "python", + by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): return model.model_dump( @@ -148,13 +149,12 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, + by_alias=by_alias, ) return cast( "dict[str, Any]", model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - exclude=exclude, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias) ), ) diff --git a/src/gcore/_utils/_json.py b/src/gcore/_utils/_json.py new file mode 100644 index 00000000..60584214 --- /dev/null +++ b/src/gcore/_utils/_json.py @@ -0,0 +1,35 @@ +import json +from typing import Any +from datetime import datetime +from typing_extensions import override + +import pydantic + +from .._compat import model_dump + + +def openapi_dumps(obj: Any) -> bytes: + """ + Serialize an object to UTF-8 encoded JSON bytes. + + Extends the standard json.dumps with support for additional types + commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc. + """ + return json.dumps( + obj, + cls=_CustomEncoder, + # Uses the same defaults as httpx's JSON serialization + ensure_ascii=False, + separators=(",", ":"), + allow_nan=False, + ).encode() + + +class _CustomEncoder(json.JSONEncoder): + @override + def default(self, o: Any) -> Any: + if isinstance(o, datetime): + return o.isoformat() + if isinstance(o, pydantic.BaseModel): + return model_dump(o, exclude_unset=True, mode="json", by_alias=True) + return super().default(o) diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py new file mode 100644 index 00000000..9e4ec0e5 --- /dev/null +++ b/tests/test_utils/test_json.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import datetime +from typing import Union + +import pydantic + +from gcore import _compat +from gcore._utils._json import openapi_dumps + + +class TestOpenapiDumps: + def test_basic(self) -> None: + data = {"key": "value", "number": 42} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"key":"value","number":42}' + + def test_datetime_serialization(self) -> None: + dt = datetime.datetime(2023, 1, 1, 12, 0, 0) + data = {"datetime": dt} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}' + + def test_pydantic_model_serialization(self) -> None: + class User(pydantic.BaseModel): + first_name: str + last_name: str + age: int + + model_instance = User(first_name="John", last_name="Kramer", age=83) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}' + + def test_pydantic_model_with_default_values(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + score: int = 0 + + model_instance = User(name="Alice") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Alice"}}' + + def test_pydantic_model_with_default_values_overridden(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + + model_instance = User(name="Bob", role="admin", active=False) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}' + + def test_pydantic_model_with_alias(self) -> None: + class User(pydantic.BaseModel): + first_name: str = pydantic.Field(alias="firstName") + last_name: str = pydantic.Field(alias="lastName") + + model_instance = User(firstName="John", lastName="Doe") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}' + + def test_pydantic_model_with_alias_and_default(self) -> None: + class User(pydantic.BaseModel): + user_name: str = pydantic.Field(alias="userName") + user_role: str = pydantic.Field(default="member", alias="userRole") + is_active: bool = pydantic.Field(default=True, alias="isActive") + + model_instance = User(userName="charlie") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"charlie"}}' + + model_with_overrides = User(userName="diana", userRole="admin", isActive=False) + data = {"model": model_with_overrides} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}' + + def test_pydantic_model_with_nested_models_and_defaults(self) -> None: + class Address(pydantic.BaseModel): + street: str + city: str = "Unknown" + + class User(pydantic.BaseModel): + name: str + address: Address + verified: bool = False + + if _compat.PYDANTIC_V1: + # to handle forward references in Pydantic v1 + User.update_forward_refs(**locals()) # type: ignore[reportDeprecated] + + address = Address(street="123 Main St") + user = User(name="Diana", address=address) + data = {"user": user} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}' + + address_with_city = Address(street="456 Oak Ave", city="Boston") + user_verified = User(name="Eve", address=address_with_city, verified=True) + data = {"user": user_verified} + json_bytes = openapi_dumps(data) + assert ( + json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}' + ) + + def test_pydantic_model_with_optional_fields(self) -> None: + class User(pydantic.BaseModel): + name: str + email: Union[str, None] + phone: Union[str, None] + + model_with_none = User(name="Eve", email=None, phone=None) + data = {"model": model_with_none} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}' + + model_with_values = User(name="Frank", email="frank@example.com", phone=None) + data = {"model": model_with_values} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}' From 026c2d041f8ccaeadb31515fbf461804e9911f63 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:57:16 +0000 Subject: [PATCH 12/12] release: 0.31.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 16 ++++++++++++++++ pyproject.toml | 2 +- src/gcore/_version.py | 2 +- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 554e34bb..f81bf992 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.30.0" + ".": "0.31.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1511c56c..5128eb5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.31.0 (2026-01-29) + +Full Changelog: [v0.30.0...v0.31.0](https://github.com/G-Core/gcore-python/compare/v0.30.0...v0.31.0) + +### Features + +* **api:** aggregated API specs update ([cd35cbf](https://github.com/G-Core/gcore-python/commit/cd35cbf266cf598045a8a4f8ebf3cf7cd8340092)) +* **api:** aggregated API specs update ([a7076d0](https://github.com/G-Core/gcore-python/commit/a7076d04fa78e4ea71b148ef8c10d0013536e904)) +* **api:** manual upload of aggregated API specs ([47734d2](https://github.com/G-Core/gcore-python/commit/47734d2b4fcd22875d2ec80b2b0453600b912eca)) +* **client:** add custom JSON encoder for extended type support ([b0c58f9](https://github.com/G-Core/gcore-python/commit/b0c58f9f042c3e67b63d99bded8179c943d29711)) + + +### Chores + +* **ci:** upgrade `actions/github-script` ([a2328c0](https://github.com/G-Core/gcore-python/commit/a2328c02b6d94f8af8cdeb7dbc72d81f1e2d17d0)) + ## 0.30.0 (2026-01-22) Full Changelog: [v0.29.0...v0.30.0](https://github.com/G-Core/gcore-python/compare/v0.29.0...v0.30.0) diff --git a/pyproject.toml b/pyproject.toml index 26af6423..3ad9b84a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "gcore" -version = "0.30.0" +version = "0.31.0" description = "The official Python library for the gcore API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/gcore/_version.py b/src/gcore/_version.py index 574d5dd3..d06a94b9 100644 --- a/src/gcore/_version.py +++ b/src/gcore/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "gcore" -__version__ = "0.30.0" # x-release-please-version +__version__ = "0.31.0" # x-release-please-version