diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/zod-openapi/README.md b/zod-openapi/README.md index b185cae..f5108dd 100644 --- a/zod-openapi/README.md +++ b/zod-openapi/README.md @@ -20,7 +20,7 @@

Speakeasy Zod OpenAPI Example

-This example Zod schema demonstrates Speakeasy-recommended practices for generating clear OpenAPI specifications optimised for creating production ready client libraries. For the associated tutorial please see our [docsite](https://speakeasyapi.dev/docs/api-frameworks/zod/). +This example Zod schema demonstrates Speakeasy-recommended practices for generating clear OpenAPI specifications optimized for creating production ready client libraries. For the associated tutorial please see our [docsite](https://www.speakeasy.com/openapi/frameworks/zod). ## Requirements @@ -33,16 +33,20 @@ To install and run this example, you'll need to clone the repository and install 1. Clone this repo: ```bash - git clone git@github.com:ritza-co/speakeasy-zod-openapi.git + git clone git@github.com:speakeasy-api/examples.git ``` -2. Install Node modules. Run the following in the terminal: +2. Change into the example directory: + ```bash + cd examples/zod-openapi + ``` +3. Install Node modules. Run the following in the terminal: ```bash npm install ``` ## Install Speakeasy CLI -To save OpenAPI output to a file and regenerate the SDK with Speakeasy, first install Speakeasy by following the [Speakeasy Getting Started](https://speakeasyapi.dev/docs/product-reference/speakeasy-cli/getting-started/) guide. +To save OpenAPI output to a file and regenerate the SDK with Speakeasy, first install Speakeasy by following the [Speakeasy Getting Started](https://www.speakeasy.com/docs/speakeasy-reference/cli/getting-started) guide. On macOS, you can install Speakeasy using Homebrew. diff --git a/zod-openapi/index.ts b/zod-openapi/index.ts index 4e17e87..0fd8e96 100644 --- a/zod-openapi/index.ts +++ b/zod-openapi/index.ts @@ -1,138 +1,78 @@ -// Following Zod v4 migration guide: https://github.com/colinhacks/zod/issues/4371 -// Using dual import strategy for incremental Zod v4 adoption - -// Import Zod v3 compatible instance for zod-openapi -import { z as z3 } from "zod"; -// Import Zod v4 for new features and future migration -import { z as z4 } from "zod/v4"; - import * as yaml from "yaml"; +import zod from "zod"; +import { ZodOpenApiOperationObject, createDocument } from "zod-openapi"; -import { - extendZodWithOpenApi, - ZodOpenApiOperationObject, - createDocument -} from "zod-openapi"; - -// Extend the Zod v3 compatible instance for zod-openapi -extendZodWithOpenApi(z3); - -// Schemas defined with z3 for current zod-openapi compatibility - - -// Step 10: Burger ID Schema with path parameter metadata -const BurgerIdSchema = z3 - .number() - .min(1) - .openapi({ - ref: "BurgerId", - description: "The unique identifier of the burger.", - example: 1, - param: { - in: "path", - name: "id", - }, - }); - -// Step 6: Burger Schema with metadata -const burgerSchema = z3.object({ - id: BurgerIdSchema, - name: z3.string().min(1).max(50).openapi({ - description: "The name of the burger.", - example: "Veggie Burger", - }), - description: z3.string().max(255).optional().openapi({ - description: "The description of the burger.", - example: "A delicious bean burger with avocado.", - }), +// Step 1: Burger ID Schema with path parameter metadata +const BurgerIdSchema = zod.number().min(1).meta({ + description: "The unique identifier of the burger.", + example: 1, + readOnly: true, }); -burgerSchema.openapi({ - ref: "Burger", - description: "A burger served at the restaurant.", -}); +// Step 2: Burger Schema with metadata +const burgerSchema = zod + .object({ + id: BurgerIdSchema, + name: zod.string().min(1).max(50).meta({ + description: "The name of the burger.", + example: "Veggie Burger", + }), + description: zod.string().max(255).optional().meta({ + description: "The description of the burger.", + example: "A delicious bean burger with avocado.", + }), + }) + .meta({ + description: "A burger served at the restaurant.", + }); -// Step 12: Create Schema for new burgers -const burgerCreateSchema = burgerSchema.omit({ id: true }).openapi({ - ref: "BurgerCreate", - description: "A burger to create.", +// Step 3: Adding Order schemas for a more complete example +const OrderIdSchema = zod.number().min(1).meta({ + description: "The unique identifier of the order.", + example: 1, + readOnly: true, }); -// Adding Order schemas for a more complete example -const OrderIdSchema = z3 - .number() - .min(1) - .openapi({ - ref: "OrderId", - description: "The unique identifier of the order.", - example: 1, - param: { - in: "path", - name: "id", - }, - }); - -const orderStatusEnum = z3.enum([ +const orderStatusEnum = zod.enum([ "pending", "in_progress", "ready", "delivered", ]); -const orderSchema = z3.object({ - id: OrderIdSchema, - burger_ids: z3 - .array(BurgerIdSchema) - .nonempty() - .openapi({ - description: "The burgers in the order.", - example: [1, 2], +const orderSchema = zod + .object({ + id: OrderIdSchema, + burger_ids: zod + .array(BurgerIdSchema) + .nonempty() + .meta({ + description: "The burgers in the order.", + example: [1, 2], + }), + time: zod.iso.datetime().meta({ + description: "The time the order was placed.", + example: "2021-01-01T00:00:00.000Z", + }), + table: zod.number().min(1).meta({ + description: "The table the order is for.", + example: 1, + }), + status: orderStatusEnum.meta({ + description: "The status of the order.", + example: "pending", + }), + note: zod.string().optional().meta({ + description: "A note for the order.", + example: "No onions.", }), - time: z3.string().datetime().openapi({ - description: "The time the order was placed.", - example: "2021-01-01T00:00:00.000Z", - }), - table: z3.number().min(1).openapi({ - description: "The table the order is for.", - example: 1, - }), - status: orderStatusEnum.openapi({ - description: "The status of the order.", - example: "pending", - }), - note: z3.string().optional().openapi({ - description: "A note for the order.", - example: "No onions.", - }), -}).openapi({ - ref: "Order", - description: "An order placed at the restaurant.", -}); - -const orderCreateSchema = orderSchema.omit({ id: true }).openapi({ - ref: "OrderCreate", - description: "An order to create.", -}); - -// Example: Demonstrating how z4 can be used for new features internally -// These schemas are NOT processed by zod-openapi in this example -const internalV4Schemas = { - userProfile: z4.object({ - username: z4.string().min(3), - email: z4.string().email(), // Using Zod v4 .email() - preferences: z4.object({ - darkMode: z4.boolean(), - notifications: z4.enum(["all", "mentions", "none"]) - }).optional(), - }), - strictObjectExample: z4.strictObject({ // Using Zod v4 .strictObject() - id: z4.string().uuid(), - value: z4.number(), }) -}; + .meta({ + description: "An order placed at the restaurant.", + }); -// API Operations defined with z3 objects for compatibility -// Step 13: Define API Operations +// API Operations defined with zod objects for compatibility +// Step 4: Define API Operations const createBurger: ZodOpenApiOperationObject = { operationId: "createBurger", summary: "Create a new burger", @@ -142,7 +82,7 @@ const createBurger: ZodOpenApiOperationObject = { description: "The burger to create.", content: { "application/json": { - schema: burgerCreateSchema, + schema: burgerSchema, }, }, }, @@ -164,7 +104,7 @@ const getBurger: ZodOpenApiOperationObject = { description: "Gets a burger from the database.", tags: ["burgers"], requestParams: { - path: z3.object({ id: BurgerIdSchema }), + path: zod.object({ id: BurgerIdSchema }), }, responses: { "200": { @@ -188,7 +128,7 @@ const listBurgers: ZodOpenApiOperationObject = { description: "The burgers were retrieved successfully.", content: { "application/json": { - schema: z3.array(burgerSchema), + schema: zod.array(burgerSchema), }, }, }, @@ -205,7 +145,7 @@ const createOrder: ZodOpenApiOperationObject = { description: "The order to create.", content: { "application/json": { - schema: orderCreateSchema, + schema: orderSchema, }, }, }, @@ -227,7 +167,7 @@ const getOrder: ZodOpenApiOperationObject = { description: "Gets an order from the database.", tags: ["orders"], requestParams: { - path: z3.object({ id: OrderIdSchema }), + path: zod.object({ id: OrderIdSchema }), }, responses: { "200": { @@ -241,7 +181,7 @@ const getOrder: ZodOpenApiOperationObject = { }, }; -// Step 14: Webhook Definition +// Step 5: Webhook Definition const createBurgerWebhook: ZodOpenApiOperationObject = { operationId: "createBurgerWebhook", summary: "New burger webhook", @@ -262,7 +202,7 @@ const createBurgerWebhook: ZodOpenApiOperationObject = { }, }; -// Step 8 & 15: Generate OpenAPI Document +// Step 6: Generate OpenAPI Document const document = createDocument({ openapi: "3.1.0", info: { @@ -299,10 +239,8 @@ const document = createDocument({ components: { schemas: { burgerSchema, - burgerCreateSchema, BurgerIdSchema, orderSchema, - orderCreateSchema, OrderIdSchema, }, }, @@ -321,4 +259,3 @@ const document = createDocument({ }); console.log(yaml.stringify(document)); - diff --git a/zod-openapi/openapi.yaml b/zod-openapi/openapi.yaml index 3d4cefd..b3d44bf 100644 --- a/zod-openapi/openapi.yaml +++ b/zod-openapi/openapi.yaml @@ -29,14 +29,14 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/BurgerCreate" + $ref: "#/components/schemas/burgerSchema" responses: "201": description: The burger was created successfully. content: application/json: schema: - $ref: "#/components/schemas/burgerSchema" + $ref: "#/components/schemas/burgerSchemaOutput" get: operationId: listBurgers summary: List burgers @@ -51,7 +51,7 @@ paths: schema: type: array items: - $ref: "#/components/schemas/burgerSchema" + $ref: "#/components/schemas/burgerSchemaOutput" /burgers/{id}: get: operationId: getBurger @@ -62,17 +62,17 @@ paths: parameters: - in: path name: id - description: The unique identifier of the burger. schema: - $ref: "#/components/schemas/BurgerId" + $ref: "#/components/schemas/BurgerIdSchema" required: true + description: The unique identifier of the burger. responses: "200": description: The burger was retrieved successfully. content: application/json: schema: - $ref: "#/components/schemas/burgerSchema" + $ref: "#/components/schemas/burgerSchemaOutput" /orders: post: operationId: createOrder @@ -85,14 +85,14 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/OrderCreate" + $ref: "#/components/schemas/orderSchema" responses: "201": description: The order was created successfully. content: application/json: schema: - $ref: "#/components/schemas/Order" + $ref: "#/components/schemas/orderSchemaOutput" /orders/{id}: get: operationId: getOrder @@ -103,17 +103,17 @@ paths: parameters: - in: path name: id - description: The unique identifier of the order. schema: - $ref: "#/components/schemas/OrderId" + $ref: "#/components/schemas/OrderIdSchema" required: true + description: The unique identifier of the order. responses: "200": description: The order was retrieved successfully. content: application/json: schema: - $ref: "#/components/schemas/Order" + $ref: "#/components/schemas/orderSchemaOutput" webhooks: /burgers: post: @@ -134,128 +134,147 @@ webhooks: components: schemas: burgerSchema: + description: A burger served at the restaurant. type: object properties: id: - $ref: "#/components/schemas/BurgerId" + $ref: "#/components/schemas/BurgerIdSchema" name: - type: string - minLength: 1 - maxLength: 50 description: The name of the burger. example: Veggie Burger - description: - type: string - maxLength: 255 - description: The description of the burger. - example: A delicious bean burger with avocado. - required: - - id - - name - BurgerCreate: - type: object - properties: - name: type: string minLength: 1 maxLength: 50 - description: The name of the burger. - example: Veggie Burger description: - type: string - maxLength: 255 description: The description of the burger. example: A delicious bean burger with avocado. + type: string + maxLength: 255 required: + - id - name - description: A burger to create. - BurgerId: - type: number - minimum: 1 + BurgerIdSchema: description: The unique identifier of the burger. example: 1 - Order: + readOnly: true + type: number + minimum: 1 + orderSchema: + description: An order placed at the restaurant. type: object properties: id: - $ref: "#/components/schemas/OrderId" + $ref: "#/components/schemas/OrderIdSchema" burger_ids: - type: array - items: - $ref: "#/components/schemas/BurgerId" - minItems: 1 description: The burgers in the order. - example: &a1 + example: - 1 - 2 + minItems: 1 + type: array + items: + $ref: "#/components/schemas/BurgerIdSchema" time: - type: string - format: date-time description: The time the order was placed. example: 2021-01-01T00:00:00.000Z + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ table: - type: number - minimum: 1 description: The table the order is for. example: 1 + type: number + minimum: 1 status: + description: The status of the order. + example: pending type: string - enum: &a2 + enum: - pending - in_progress - ready - delivered - description: The status of the order. - example: pending note: - type: string description: A note for the order. example: No onions. + type: string required: - id - burger_ids - time - table - status + OrderIdSchema: + description: The unique identifier of the order. + example: 1 + readOnly: true + type: number + minimum: 1 + burgerSchemaOutput: + description: A burger served at the restaurant. + type: object + properties: + id: + $ref: "#/components/schemas/BurgerIdSchema" + name: + description: The name of the burger. + example: Veggie Burger + type: string + minLength: 1 + maxLength: 50 + description: + description: The description of the burger. + example: A delicious bean burger with avocado. + type: string + maxLength: 255 + required: + - id + - name + additionalProperties: false + orderSchemaOutput: description: An order placed at the restaurant. - OrderCreate: type: object properties: + id: + $ref: "#/components/schemas/OrderIdSchema" burger_ids: + description: The burgers in the order. + example: + - 1 + - 2 + minItems: 1 type: array items: - $ref: "#/components/schemas/BurgerId" - minItems: 1 - description: The burgers in the order. - example: *a1 + $ref: "#/components/schemas/BurgerIdSchema" time: - type: string - format: date-time description: The time the order was placed. example: 2021-01-01T00:00:00.000Z + type: string + format: date-time + pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ table: - type: number - minimum: 1 description: The table the order is for. example: 1 + type: number + minimum: 1 status: - type: string - enum: *a2 description: The status of the order. example: pending - note: type: string + enum: + - pending + - in_progress + - ready + - delivered + note: description: A note for the order. example: No onions. + type: string required: + - id - burger_ids - time - table - status - description: An order to create. - OrderId: - type: number - minimum: 1 - description: The unique identifier of the order. - example: 1 + additionalProperties: false diff --git a/zod-openapi/package-lock.json b/zod-openapi/package-lock.json index 453a122..2fb3e8c 100644 --- a/zod-openapi/package-lock.json +++ b/zod-openapi/package-lock.json @@ -1,24 +1,571 @@ { "name": "speakeasy-zod-openapi", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "speakeasy-zod-openapi", - "version": "1.0.0", + "version": "2.0.0", "license": "ISC", "dependencies": { - "typescript": "^5.1.6", - "yaml": "^2.3.2", - "zod": "^3.21.4", - "zod-openapi": "^2.7.4" + "yaml": "^2.8.0", + "zod": "^4.1.0", + "zod-openapi": "^5.3.0" + }, + "devDependencies": { + "tsx": "^4.20.0", + "typescript": "^5.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" } }, "node_modules/typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -28,30 +575,39 @@ } }, "node_modules/yaml": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", - "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 14" + "node": ">= 14.6" } }, "node_modules/zod": { - "version": "3.21.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", - "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-openapi": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/zod-openapi/-/zod-openapi-2.7.4.tgz", - "integrity": "sha512-rL7Yj/53snsgYoSNLdIpU86XRgIFW6twixHOwjPvuHm/yMt8pV+BE1tO/7AWOWk0kwQbAYPwZajbY/CYHROp3w==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/zod-openapi/-/zod-openapi-5.4.3.tgz", + "integrity": "sha512-6kJ/gJdvHZtuxjYHoMtkl2PixCwRuZ/s79dVkEr7arHvZGXfx7Cvh53X3HfJ5h9FzGelXOXlnyjwfX0sKEPByw==", + "license": "MIT", "engines": { - "node": ">=16.11" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/samchungy/zod-openapi?sponsor=1" }, "peerDependencies": { - "zod": "^3.21.4" + "zod": "^3.25.74 || ^4.0.0" } } } diff --git a/zod-openapi/package.json b/zod-openapi/package.json index cd1bf7a..55bfb2e 100644 --- a/zod-openapi/package.json +++ b/zod-openapi/package.json @@ -1,6 +1,6 @@ { "name": "speakeasy-zod-openapi", - "version": "1.0.0", + "version": "2.0.0", "description": "A Zod OpenAPI Example with Speakeasy Integration", "main": "index.js", "scripts": { @@ -12,11 +12,11 @@ "license": "ISC", "dependencies": { "yaml": "^2.8.0", - "zod": "^3.25.28", - "zod-openapi": "^4.2.4" + "zod": "^4.1.0", + "zod-openapi": "^5.3.0" }, "devDependencies": { - "tsx": "^4.19.4", - "typescript": "^5.8.3" + "tsx": "^4.20.0", + "typescript": "^5.9.0" } } diff --git a/zod-openapi/sdk/.gitattributes b/zod-openapi/sdk/.gitattributes new file mode 100644 index 0000000..4d75d59 --- /dev/null +++ b/zod-openapi/sdk/.gitattributes @@ -0,0 +1,2 @@ +# This allows generated code to be indexed correctly +*.py linguist-generated=false \ No newline at end of file diff --git a/zod-openapi/sdk/.gitignore b/zod-openapi/sdk/.gitignore index 3cdf338..a5a67fa 100755 --- a/zod-openapi/sdk/.gitignore +++ b/zod-openapi/sdk/.gitignore @@ -1,3 +1,11 @@ +.venv/ +**/__pycache__/ +pyrightconfig.json +**/.speakeasy/temp/ +**/.speakeasy/logs/ +.speakeasy/reports +.env +.env.local venv/ src/*.egg-info/ __pycache__/ diff --git a/zod-openapi/sdk/.speakeasy/gen.lock b/zod-openapi/sdk/.speakeasy/gen.lock new file mode 100644 index 0000000..afa5b04 --- /dev/null +++ b/zod-openapi/sdk/.speakeasy/gen.lock @@ -0,0 +1,129 @@ +lockVersion: 2.0.0 +id: ff0e8d69-7b63-41ac-a62d-6308b3b64b6c +management: + docChecksum: c213754a91485b968716aada1011c873 + docVersion: 1.0.0 + speakeasyVersion: 1.639.3 + generationVersion: 2.730.5 + releaseVersion: 0.2.0 + configChecksum: d792ca790690520ddd07aba61c8cfb17 +features: + python: + additionalDependencies: 1.0.0 + core: 5.23.0 + defaultEnabledRetries: 0.2.0 + enumUnions: 0.1.0 + envVarSecurityUsage: 0.3.2 + globalSecurityCallbacks: 1.0.0 + globalServerURLs: 3.1.1 + inputOutputModels: 3.0.0 + methodArguments: 1.0.2 + responseFormat: 1.0.1 + retries: 3.0.2 + sdkHooks: 1.1.0 + webhooks: 2.0.0 +generatedFiles: + - .gitattributes + - .vscode/settings.json + - USAGE.md + - docs/models/burgerschema.md + - docs/models/burgerschemaoutput.md + - docs/models/getburgerrequest.md + - docs/models/getorderrequest.md + - docs/models/orderschema.md + - docs/models/orderschemaoutput.md + - docs/models/orderschemaoutputstatus.md + - docs/models/status.md + - docs/models/utils/retryconfig.md + - docs/sdks/burgers/README.md + - docs/sdks/orders/README.md + - poetry.toml + - py.typed + - pylintrc + - pyproject.toml + - scripts/publish.sh + - src/openapi/__init__.py + - src/openapi/_hooks/__init__.py + - src/openapi/_hooks/sdkhooks.py + - src/openapi/_hooks/types.py + - src/openapi/_version.py + - src/openapi/basesdk.py + - src/openapi/burgers.py + - src/openapi/errors/__init__.py + - src/openapi/errors/no_response_error.py + - src/openapi/errors/responsevalidationerror.py + - src/openapi/errors/sdkbaseerror.py + - src/openapi/errors/sdkerror.py + - src/openapi/httpclient.py + - src/openapi/models/__init__.py + - src/openapi/models/burgerschema.py + - src/openapi/models/burgerschemaoutput.py + - src/openapi/models/getburgerop.py + - src/openapi/models/getorderop.py + - src/openapi/models/orderschema.py + - src/openapi/models/orderschemaoutput.py + - src/openapi/orders.py + - src/openapi/py.typed + - src/openapi/sdk.py + - src/openapi/sdkconfiguration.py + - src/openapi/types/__init__.py + - src/openapi/types/basemodel.py + - src/openapi/utils/__init__.py + - src/openapi/utils/annotations.py + - src/openapi/utils/datetimes.py + - src/openapi/utils/enums.py + - src/openapi/utils/eventstreaming.py + - src/openapi/utils/forms.py + - src/openapi/utils/headers.py + - src/openapi/utils/logger.py + - src/openapi/utils/metadata.py + - src/openapi/utils/queryparams.py + - src/openapi/utils/requestbodies.py + - src/openapi/utils/retries.py + - src/openapi/utils/security.py + - src/openapi/utils/serializers.py + - src/openapi/utils/unmarshal_json_response.py + - src/openapi/utils/url.py + - src/openapi/utils/values.py +examples: + createBurger: + speakeasy-default-create-burger: + requestBody: + application/json: {"description": "A delicious bean burger with avocado.", "name": "Veggie Burger"} + responses: + "201": + application/json: {"description": "A delicious bean burger with avocado.", "id": 1, "name": "Veggie Burger"} + getBurger: + speakeasy-default-get-burger: + parameters: + path: + id: 1 + responses: + "200": + application/json: {"description": "A delicious bean burger with avocado.", "id": 1, "name": "Veggie Burger"} + listBurgers: + speakeasy-default-list-burgers: + responses: + "200": + application/json: [{"description": "A delicious bean burger with avocado.", "id": 1, "name": "Veggie Burger"}] + createBurgerWebhook: + speakeasy-default-create-burger-webhook: + requestBody: + application/json: {"description": "A delicious bean burger with avocado.", "name": "Veggie Burger"} + createOrder: + speakeasy-default-create-order: + requestBody: + application/json: {"burger_ids": [1, 2], "note": "No onions.", "status": "pending", "table": 1, "time": "2021-01-01T00:00:00Z"} + responses: + "201": + application/json: {"burger_ids": [1, 2], "id": 1, "note": "No onions.", "status": "pending", "table": 1, "time": "2021-01-01T00:00:00Z"} + getOrder: + speakeasy-default-get-order: + parameters: + path: + id: 1 + responses: + "200": + application/json: {"burger_ids": [1, 2], "id": 1, "note": "No onions.", "status": "pending", "table": 1, "time": "2021-01-01T00:00:00Z"} +examplesVersion: 1.0.2 +generatedTests: {} diff --git a/zod-openapi/sdk/.vscode/settings.json b/zod-openapi/sdk/.vscode/settings.json new file mode 100644 index 0000000..8d79f0a --- /dev/null +++ b/zod-openapi/sdk/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "python.testing.pytestArgs": ["tests", "-vv"], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "pylint.args": ["--rcfile=pylintrc"] +} diff --git a/zod-openapi/sdk/CONTRIBUTING.md b/zod-openapi/sdk/CONTRIBUTING.md new file mode 100644 index 0000000..d585717 --- /dev/null +++ b/zod-openapi/sdk/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# Contributing to This Repository + +Thank you for your interest in contributing to this repository. Please note that this repository contains generated code. As such, we do not accept direct changes or pull requests. Instead, we encourage you to follow the guidelines below to report issues and suggest improvements. + +## How to Report Issues + +If you encounter any bugs or have suggestions for improvements, please open an issue on GitHub. When reporting an issue, please provide as much detail as possible to help us reproduce the problem. This includes: + +- A clear and descriptive title +- Steps to reproduce the issue +- Expected and actual behavior +- Any relevant logs, screenshots, or error messages +- Information about your environment (e.g., operating system, software versions) + - For example can be collected using the `npx envinfo` command from your terminal if you have Node.js installed + +## Issue Triage and Upstream Fixes + +We will review and triage issues as quickly as possible. Our goal is to address bugs and incorporate improvements in the upstream source code. Fixes will be included in the next generation of the generated code. + +## Contact + +If you have any questions or need further assistance, please feel free to reach out by opening an issue. + +Thank you for your understanding and cooperation! + +The Maintainers diff --git a/zod-openapi/sdk/README.md b/zod-openapi/sdk/README.md index 64f76fc..e0eb30f 100755 --- a/zod-openapi/sdk/README.md +++ b/zod-openapi/sdk/README.md @@ -1,55 +1,144 @@ # openapi - + ## SDK Installation +> [!TIP] +> To finish publishing your SDK to PyPI you must [run your first generation action](https://www.speakeasy.com/docs/github-setup#step-by-step-guide). + + +> [!NOTE] +> **Python version upgrade policy** +> +> Once a Python version reaches its [official end of life date](https://devguide.python.org/versions/), a 3-month grace period is provided for users to upgrade. Following this grace period, the minimum python version supported in the SDK will be updated. + +The SDK can be installed with *uv*, *pip*, or *poetry* package managers. + +### uv + +*uv* is a fast Python package installer and resolver, designed as a drop-in replacement for pip and pip-tools. It's recommended for its speed and modern Python tooling capabilities. + +```bash +uv add git+.git +``` + +### PIP + +*PIP* is the default package installer for Python, enabling easy installation and management of packages from PyPI via the command line. + ```bash pip install git+.git ``` - -## SDK Example Usage - +### Poetry + +*Poetry* is a modern tool that simplifies dependency management and package publishing by using a single `pyproject.toml` file to handle project metadata and dependencies. + +```bash +poetry add git+.git +``` + +### Shell and script usage with `uv` +You can use this SDK in a Python shell with [uv](https://docs.astral.sh/uv/) and the `uvx` command that comes with it like so: + +```shell +uvx --from openapi python +``` + +It's also possible to write a standalone Python script without needing to set up a whole project like so: ```python -import sdk -from sdk.models import shared +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.9" +# dependencies = [ +# "openapi", +# ] +# /// -s = sdk.SDK() +from openapi import SDK -req = shared.BurgerCreate( - description='A delicious bean burger with avocado.', - name='Veggie Burger', +sdk = SDK( + # SDK arguments ) -res = s.burgers.create_burger(req) +# Rest of script here... +``` + +Once that is saved to a file, you can run it with `uv run script.py` where +`script.py` can be replaced with the actual file name. + + + +## SDK Example Usage + +### Example + +```python +# Synchronous Example +from openapi import SDK + + +with SDK() as sdk: + + res = sdk.burgers.create_burger(request={ + "description": "A delicious bean burger with avocado.", + "name": "Veggie Burger", + }) + + assert res is not None + + # Handle response + print(res) +``` + +
-if res.burger is not None: - # handle response +The same SDK client can also be used to make asynchronous requests by importing asyncio. + +```python +# Asynchronous Example +import asyncio +from openapi import SDK + +async def main(): + + async with SDK() as sdk: + + res = await sdk.burgers.create_burger_async(request={ + "description": "A delicious bean burger with avocado.", + "name": "Veggie Burger", + }) + + assert res is not None + + # Handle response + print(res) + +asyncio.run(main()) ``` - + - + ## Available Resources and Operations +
+Available methods ### [burgers](docs/sdks/burgers/README.md) * [create_burger](docs/sdks/burgers/README.md#create_burger) - Create a new burger -* [delete_burger](docs/sdks/burgers/README.md#delete_burger) - Delete a burger * [get_burger](docs/sdks/burgers/README.md#get_burger) - Get a burger * [list_burgers](docs/sdks/burgers/README.md#list_burgers) - List burgers -* [update_burger](docs/sdks/burgers/README.md#update_burger) - Update a burger ### [orders](docs/sdks/orders/README.md) * [create_order](docs/sdks/orders/README.md#create_order) - Create a new order -* [delete_order](docs/sdks/orders/README.md#delete_order) - Delete an order * [get_order](docs/sdks/orders/README.md#get_order) - Get an order -* [list_orders](docs/sdks/orders/README.md#list_orders) - List orders -* [update_order](docs/sdks/orders/README.md#update_order) - Update an order - + +
+ ### Maturity @@ -63,3 +152,296 @@ While we value open-source contributions to this SDK, this library is generated Feel free to open a PR or a Github issue as a proof of concept and we'll do our best to include it in a future release! ### SDK Created by [Speakeasy](https://docs.speakeasyapi.dev/docs/using-speakeasy/client-sdks) + + +## Summary + +Burger Restaurant API: An API for managing burgers and orders at a restaurant. + + + +## Table of Contents + +* [openapi](#openapi) + * [SDK Installation](#sdk-installation) + * [SDK Example Usage](#sdk-example-usage) + * [Available Resources and Operations](#available-resources-and-operations) + * [IDE Support](#ide-support) + * [Retries](#retries) + * [Error Handling](#error-handling) + * [Server Selection](#server-selection) + * [Custom HTTP Client](#custom-http-client) + * [Resource Management](#resource-management) + * [Debugging](#debugging) + + + + +## IDE Support + +### PyCharm + +Generally, the SDK will work well with most IDEs out of the box. However, when using PyCharm, you can enjoy much better integration with Pydantic by installing an additional plugin. + +- [PyCharm Pydantic Plugin](https://docs.pydantic.dev/latest/integrations/pycharm/) + + + +## Retries + +Some of the endpoints in this SDK support retries. If you use the SDK without any configuration, it will fall back to the default retry strategy provided by the API. However, the default retry strategy can be overridden on a per-operation basis, or across the entire SDK. + +To change the default retry strategy for a single API call, simply provide a `RetryConfig` object to the call: +```python +from openapi import SDK +from openapi.utils import BackoffStrategy, RetryConfig + + +with SDK() as sdk: + + res = sdk.burgers.create_burger(request={ + "description": "A delicious bean burger with avocado.", + "name": "Veggie Burger", + }, + RetryConfig("backoff", BackoffStrategy(1, 50, 1.1, 100), False)) + + assert res is not None + + # Handle response + print(res) + +``` + +If you'd like to override the default retry strategy for all operations that support retries, you can use the `retry_config` optional parameter when initializing the SDK: +```python +from openapi import SDK +from openapi.utils import BackoffStrategy, RetryConfig + + +with SDK( + retry_config=RetryConfig("backoff", BackoffStrategy(1, 50, 1.1, 100), False), +) as sdk: + + res = sdk.burgers.create_burger(request={ + "description": "A delicious bean burger with avocado.", + "name": "Veggie Burger", + }) + + assert res is not None + + # Handle response + print(res) + +``` + + + +## Error Handling + +[`SDKBaseError`](./src/openapi/errors/sdkbaseerror.py) is the base class for all HTTP error responses. It has the following properties: + +| Property | Type | Description | +| ------------------ | ---------------- | ------------------------------------------------------ | +| `err.message` | `str` | Error message | +| `err.status_code` | `int` | HTTP response status code eg `404` | +| `err.headers` | `httpx.Headers` | HTTP response headers | +| `err.body` | `str` | HTTP body. Can be empty string if no body is returned. | +| `err.raw_response` | `httpx.Response` | Raw HTTP response | + +### Example +```python +from openapi import SDK, errors + + +with SDK() as sdk: + res = None + try: + + res = sdk.burgers.create_burger(request={ + "description": "A delicious bean burger with avocado.", + "name": "Veggie Burger", + }) + + assert res is not None + + # Handle response + print(res) + + + except errors.SDKBaseError as e: + # The base class for HTTP error responses + print(e.message) + print(e.status_code) + print(e.body) + print(e.headers) + print(e.raw_response) + +``` + +### Error Classes +**Primary error:** +* [`SDKBaseError`](./src/openapi/errors/sdkbaseerror.py): The base class for HTTP error responses. + +
Less common errors (5) + +
+ +**Network errors:** +* [`httpx.RequestError`](https://www.python-httpx.org/exceptions/#httpx.RequestError): Base class for request errors. + * [`httpx.ConnectError`](https://www.python-httpx.org/exceptions/#httpx.ConnectError): HTTP client was unable to make a request to a server. + * [`httpx.TimeoutException`](https://www.python-httpx.org/exceptions/#httpx.TimeoutException): HTTP request timed out. + + +**Inherit from [`SDKBaseError`](./src/openapi/errors/sdkbaseerror.py)**: +* [`ResponseValidationError`](./src/openapi/errors/responsevalidationerror.py): Type mismatch between the response data and the expected Pydantic model. Provides access to the Pydantic validation error via the `cause` attribute. + +
+ + + +## Server Selection + +### Override Server URL Per-Client + +The default server can be overridden globally by passing a URL to the `server_url: str` optional parameter when initializing the SDK client instance. For example: +```python +from openapi import SDK + + +with SDK( + server_url="https://example.com", +) as sdk: + + res = sdk.burgers.create_burger(request={ + "description": "A delicious bean burger with avocado.", + "name": "Veggie Burger", + }) + + assert res is not None + + # Handle response + print(res) + +``` + + + +## Custom HTTP Client + +The Python SDK makes API calls using the [httpx](https://www.python-httpx.org/) HTTP library. In order to provide a convenient way to configure timeouts, cookies, proxies, custom headers, and other low-level configuration, you can initialize the SDK client with your own HTTP client instance. +Depending on whether you are using the sync or async version of the SDK, you can pass an instance of `HttpClient` or `AsyncHttpClient` respectively, which are Protocol's ensuring that the client has the necessary methods to make API calls. +This allows you to wrap the client with your own custom logic, such as adding custom headers, logging, or error handling, or you can just pass an instance of `httpx.Client` or `httpx.AsyncClient` directly. + +For example, you could specify a header for every request that this sdk makes as follows: +```python +from openapi import SDK +import httpx + +http_client = httpx.Client(headers={"x-custom-header": "someValue"}) +s = SDK(client=http_client) +``` + +or you could wrap the client with your own custom logic: +```python +from openapi import SDK +from openapi.httpclient import AsyncHttpClient +import httpx + +class CustomClient(AsyncHttpClient): + client: AsyncHttpClient + + def __init__(self, client: AsyncHttpClient): + self.client = client + + async def send( + self, + request: httpx.Request, + *, + stream: bool = False, + auth: Union[ + httpx._types.AuthTypes, httpx._client.UseClientDefault, None + ] = httpx.USE_CLIENT_DEFAULT, + follow_redirects: Union[ + bool, httpx._client.UseClientDefault + ] = httpx.USE_CLIENT_DEFAULT, + ) -> httpx.Response: + request.headers["Client-Level-Header"] = "added by client" + + return await self.client.send( + request, stream=stream, auth=auth, follow_redirects=follow_redirects + ) + + def build_request( + self, + method: str, + url: httpx._types.URLTypes, + *, + content: Optional[httpx._types.RequestContent] = None, + data: Optional[httpx._types.RequestData] = None, + files: Optional[httpx._types.RequestFiles] = None, + json: Optional[Any] = None, + params: Optional[httpx._types.QueryParamTypes] = None, + headers: Optional[httpx._types.HeaderTypes] = None, + cookies: Optional[httpx._types.CookieTypes] = None, + timeout: Union[ + httpx._types.TimeoutTypes, httpx._client.UseClientDefault + ] = httpx.USE_CLIENT_DEFAULT, + extensions: Optional[httpx._types.RequestExtensions] = None, + ) -> httpx.Request: + return self.client.build_request( + method, + url, + content=content, + data=data, + files=files, + json=json, + params=params, + headers=headers, + cookies=cookies, + timeout=timeout, + extensions=extensions, + ) + +s = SDK(async_client=CustomClient(httpx.AsyncClient())) +``` + + + +## Resource Management + +The `SDK` class implements the context manager protocol and registers a finalizer function to close the underlying sync and async HTTPX clients it uses under the hood. This will close HTTP connections, release memory and free up other resources held by the SDK. In short-lived Python programs and notebooks that make a few SDK method calls, resource management may not be a concern. However, in longer-lived programs, it is beneficial to create a single SDK instance via a [context manager][context-manager] and reuse it across the application. + +[context-manager]: https://docs.python.org/3/reference/datamodel.html#context-managers + +```python +from openapi import SDK +def main(): + + with SDK() as sdk: + # Rest of application here... + + +# Or when using async: +async def amain(): + + async with SDK() as sdk: + # Rest of application here... +``` + + + +## Debugging + +You can setup your SDK to emit debug logs for SDK requests and responses. + +You can pass your own logger class directly into your SDK. +```python +from openapi import SDK +import logging + +logging.basicConfig(level=logging.DEBUG) +s = SDK(debug_logger=logging.getLogger("openapi")) +``` + + + diff --git a/zod-openapi/sdk/USAGE.md b/zod-openapi/sdk/USAGE.md index 18432a1..81b3aee 100755 --- a/zod-openapi/sdk/USAGE.md +++ b/zod-openapi/sdk/USAGE.md @@ -1,20 +1,45 @@ - + +```python +# Synchronous Example +from openapi import SDK + + +with SDK() as sdk: + + res = sdk.burgers.create_burger(request={ + "description": "A delicious bean burger with avocado.", + "name": "Veggie Burger", + }) + + assert res is not None + # Handle response + print(res) +``` + +
+ +The same SDK client can also be used to make asynchronous requests by importing asyncio. ```python -import sdk -from sdk.models import shared +# Asynchronous Example +import asyncio +from openapi import SDK + +async def main(): + + async with SDK() as sdk: -s = sdk.SDK() + res = await sdk.burgers.create_burger_async(request={ + "description": "A delicious bean burger with avocado.", + "name": "Veggie Burger", + }) -req = shared.BurgerCreate( - description='A delicious bean burger with avocado.', - name='Veggie Burger', -) + assert res is not None -res = s.burgers.create_burger(req) + # Handle response + print(res) -if res.burger is not None: - # handle response +asyncio.run(main()) ``` - \ No newline at end of file + \ No newline at end of file diff --git a/zod-openapi/sdk/docs/models/burgerschema.md b/zod-openapi/sdk/docs/models/burgerschema.md new file mode 100644 index 0000000..492c458 --- /dev/null +++ b/zod-openapi/sdk/docs/models/burgerschema.md @@ -0,0 +1,11 @@ +# BurgerSchema + +A burger served at the restaurant. + + +## Fields + +| Field | Type | Required | Description | Example | +| ------------------------------------- | ------------------------------------- | ------------------------------------- | ------------------------------------- | ------------------------------------- | +| `description` | *Optional[str]* | :heavy_minus_sign: | The description of the burger. | A delicious bean burger with avocado. | +| `name` | *str* | :heavy_check_mark: | The name of the burger. | Veggie Burger | \ No newline at end of file diff --git a/zod-openapi/sdk/docs/models/burgerschemaoutput.md b/zod-openapi/sdk/docs/models/burgerschemaoutput.md new file mode 100644 index 0000000..9c3a2c4 --- /dev/null +++ b/zod-openapi/sdk/docs/models/burgerschemaoutput.md @@ -0,0 +1,12 @@ +# BurgerSchemaOutput + +A burger served at the restaurant. + + +## Fields + +| Field | Type | Required | Description | Example | +| ------------------------------------- | ------------------------------------- | ------------------------------------- | ------------------------------------- | ------------------------------------- | +| `description` | *Optional[str]* | :heavy_minus_sign: | The description of the burger. | A delicious bean burger with avocado. | +| `id` | *float* | :heavy_check_mark: | The unique identifier of the burger. | 1 | +| `name` | *str* | :heavy_check_mark: | The name of the burger. | Veggie Burger | \ No newline at end of file diff --git a/zod-openapi/sdk/docs/models/getburgerrequest.md b/zod-openapi/sdk/docs/models/getburgerrequest.md new file mode 100644 index 0000000..15fd48d --- /dev/null +++ b/zod-openapi/sdk/docs/models/getburgerrequest.md @@ -0,0 +1,8 @@ +# GetBurgerRequest + + +## Fields + +| Field | Type | Required | Description | Example | +| ------------------------------------ | ------------------------------------ | ------------------------------------ | ------------------------------------ | ------------------------------------ | +| `id` | *float* | :heavy_check_mark: | The unique identifier of the burger. | 1 | \ No newline at end of file diff --git a/zod-openapi/sdk/docs/models/getorderrequest.md b/zod-openapi/sdk/docs/models/getorderrequest.md new file mode 100644 index 0000000..870646b --- /dev/null +++ b/zod-openapi/sdk/docs/models/getorderrequest.md @@ -0,0 +1,8 @@ +# GetOrderRequest + + +## Fields + +| Field | Type | Required | Description | Example | +| ----------------------------------- | ----------------------------------- | ----------------------------------- | ----------------------------------- | ----------------------------------- | +| `id` | *float* | :heavy_check_mark: | The unique identifier of the order. | 1 | \ No newline at end of file diff --git a/zod-openapi/sdk/docs/models/orderschema.md b/zod-openapi/sdk/docs/models/orderschema.md new file mode 100644 index 0000000..3d59943 --- /dev/null +++ b/zod-openapi/sdk/docs/models/orderschema.md @@ -0,0 +1,14 @@ +# OrderSchema + +An order placed at the restaurant. + + +## Fields + +| Field | Type | Required | Description | Example | +| -------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | +| `burger_ids` | List[*float*] | :heavy_check_mark: | The burgers in the order. | [
1,
2
] | +| `note` | *Optional[str]* | :heavy_minus_sign: | A note for the order. | No onions. | +| `status` | [models.Status](../models/status.md) | :heavy_check_mark: | The status of the order. | pending | +| `table` | *float* | :heavy_check_mark: | The table the order is for. | 1 | +| `time` | [date](https://docs.python.org/3/library/datetime.html#date-objects) | :heavy_check_mark: | The time the order was placed. | 2021-01-01 00:00:00 +0000 UTC | \ No newline at end of file diff --git a/zod-openapi/sdk/docs/models/orderschemaoutput.md b/zod-openapi/sdk/docs/models/orderschemaoutput.md new file mode 100644 index 0000000..b9ab373 --- /dev/null +++ b/zod-openapi/sdk/docs/models/orderschemaoutput.md @@ -0,0 +1,15 @@ +# OrderSchemaOutput + +An order placed at the restaurant. + + +## Fields + +| Field | Type | Required | Description | Example | +| ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| `burger_ids` | List[*float*] | :heavy_check_mark: | The burgers in the order. | [
1,
2
] | +| `id` | *float* | :heavy_check_mark: | The unique identifier of the order. | 1 | +| `note` | *Optional[str]* | :heavy_minus_sign: | A note for the order. | No onions. | +| `status` | [models.OrderSchemaOutputStatus](../models/orderschemaoutputstatus.md) | :heavy_check_mark: | The status of the order. | pending | +| `table` | *float* | :heavy_check_mark: | The table the order is for. | 1 | +| `time` | [date](https://docs.python.org/3/library/datetime.html#date-objects) | :heavy_check_mark: | The time the order was placed. | 2021-01-01 00:00:00 +0000 UTC | \ No newline at end of file diff --git a/zod-openapi/sdk/docs/models/orderschemaoutputstatus.md b/zod-openapi/sdk/docs/models/orderschemaoutputstatus.md new file mode 100644 index 0000000..8389e25 --- /dev/null +++ b/zod-openapi/sdk/docs/models/orderschemaoutputstatus.md @@ -0,0 +1,13 @@ +# OrderSchemaOutputStatus + +The status of the order. + + +## Values + +| Name | Value | +| ------------- | ------------- | +| `PENDING` | pending | +| `IN_PROGRESS` | in_progress | +| `READY` | ready | +| `DELIVERED` | delivered | \ No newline at end of file diff --git a/zod-openapi/sdk/docs/models/status.md b/zod-openapi/sdk/docs/models/status.md new file mode 100644 index 0000000..9f80588 --- /dev/null +++ b/zod-openapi/sdk/docs/models/status.md @@ -0,0 +1,13 @@ +# Status + +The status of the order. + + +## Values + +| Name | Value | +| ------------- | ------------- | +| `PENDING` | pending | +| `IN_PROGRESS` | in_progress | +| `READY` | ready | +| `DELIVERED` | delivered | \ No newline at end of file diff --git a/zod-openapi/sdk/docs/sdks/burgers/README.md b/zod-openapi/sdk/docs/sdks/burgers/README.md index 1eefae6..001f725 100755 --- a/zod-openapi/sdk/docs/sdks/burgers/README.md +++ b/zod-openapi/sdk/docs/sdks/burgers/README.md @@ -1,16 +1,13 @@ -# burgers +# Burgers +(*burgers*) ## Overview -Operations for managing burgers. - ### Available Operations * [create_burger](#create_burger) - Create a new burger -* [delete_burger](#delete_burger) - Delete a burger * [get_burger](#get_burger) - Get a burger * [list_burgers](#list_burgers) - List burgers -* [update_burger](#update_burger) - Update a burger ## create_burger @@ -18,70 +15,41 @@ Creates a new burger in the database. ### Example Usage + ```python -import sdk -from sdk.models import shared +from openapi import SDK + -s = sdk.SDK() +with SDK() as sdk: -req = shared.BurgerCreate( - description='A delicious bean burger with avocado.', - name='Veggie Burger', -) + res = sdk.burgers.create_burger(request={ + "description": "A delicious bean burger with avocado.", + "name": "Veggie Burger", + }) -res = s.burgers.create_burger(req) + assert res is not None + + # Handle response + print(res) -if res.burger is not None: - # handle response ``` ### Parameters | Parameter | Type | Required | Description | | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | -| `request` | [shared.BurgerCreate](../../models/shared/burgercreate.md) | :heavy_check_mark: | The request object to use for the request. | +| `request` | [models.BurgerSchema](../../models/burgerschema.md) | :heavy_check_mark: | The request object to use for the request. | | `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | - ### Response -**[operations.CreateBurgerResponse](../../models/operations/createburgerresponse.md)** +**[models.BurgerSchemaOutput](../../models/burgerschemaoutput.md)** +### Errors -## delete_burger - -Deletes a burger from the database. - -### Example Usage - -```python -import sdk -from sdk.models import operations - -s = sdk.SDK() - -req = operations.DeleteBurgerRequest( - id=1, -) - -res = s.burgers.delete_burger(req) - -if res.status_code == 200: - # handle response -``` - -### Parameters - -| Parameter | Type | Required | Description | -| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -| `request` | [operations.DeleteBurgerRequest](../../models/operations/deleteburgerrequest.md) | :heavy_check_mark: | The request object to use for the request. | -| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | - - -### Response - -**[operations.DeleteBurgerResponse](../../models/operations/deleteburgerresponse.md)** - +| Error Type | Status Code | Content Type | +| --------------- | --------------- | --------------- | +| errors.SDKError | 4XX, 5XX | \*/\* | ## get_burger @@ -89,34 +57,40 @@ Gets a burger from the database. ### Example Usage + ```python -import sdk -from sdk.models import operations +from openapi import SDK -s = sdk.SDK() -req = operations.GetBurgerRequest( - id=1, -) +with SDK() as sdk: -res = s.burgers.get_burger(req) + res = sdk.burgers.get_burger(request={ + "id": 1, + }) + + assert res is not None + + # Handle response + print(res) -if res.burger is not None: - # handle response ``` ### Parameters -| Parameter | Type | Required | Description | -| -------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -| `request` | [operations.GetBurgerRequest](../../models/operations/getburgerrequest.md) | :heavy_check_mark: | The request object to use for the request. | -| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | - +| Parameter | Type | Required | Description | +| ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | +| `request` | [models.GetBurgerRequest](../../models/getburgerrequest.md) | :heavy_check_mark: | The request object to use for the request. | +| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | ### Response -**[operations.GetBurgerResponse](../../models/operations/getburgerresponse.md)** +**[models.BurgerSchemaOutput](../../models/burgerschemaoutput.md)** +### Errors + +| Error Type | Status Code | Content Type | +| --------------- | --------------- | --------------- | +| errors.SDKError | 4XX, 5XX | \*/\* | ## list_burgers @@ -124,17 +98,20 @@ Lists all burgers in the database. ### Example Usage + ```python -import sdk +from openapi import SDK + +with SDK() as sdk: -s = sdk.SDK() + res = sdk.burgers.list_burgers() + assert res is not None -res = s.burgers.list_burgers() + # Handle response + print(res) -if res.burgers is not None: - # handle response ``` ### Parameters @@ -143,47 +120,12 @@ if res.burgers is not None: | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | | `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | - ### Response -**[operations.ListBurgersResponse](../../models/operations/listburgersresponse.md)** - - -## update_burger - -Updates a burger in the database. - -### Example Usage - -```python -import sdk -from sdk.models import operations, shared - -s = sdk.SDK() - -req = operations.UpdateBurgerRequest( - burger_update=shared.BurgerUpdate( - description='A delicious bean burger with avocado.', - name='Veggie Burger', - ), - id=1, -) - -res = s.burgers.update_burger(req) - -if res.burger is not None: - # handle response -``` - -### Parameters - -| Parameter | Type | Required | Description | -| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -| `request` | [operations.UpdateBurgerRequest](../../models/operations/updateburgerrequest.md) | :heavy_check_mark: | The request object to use for the request. | -| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | - - -### Response +**[List[models.BurgerSchemaOutput]](../../models/.md)** -**[operations.UpdateBurgerResponse](../../models/operations/updateburgerresponse.md)** +### Errors +| Error Type | Status Code | Content Type | +| --------------- | --------------- | --------------- | +| errors.SDKError | 4XX, 5XX | \*/\* | \ No newline at end of file diff --git a/zod-openapi/sdk/docs/sdks/orders/README.md b/zod-openapi/sdk/docs/sdks/orders/README.md index 3add115..74c8768 100755 --- a/zod-openapi/sdk/docs/sdks/orders/README.md +++ b/zod-openapi/sdk/docs/sdks/orders/README.md @@ -1,16 +1,12 @@ -# orders +# Orders +(*orders*) ## Overview -Operations for managing orders. - ### Available Operations * [create_order](#create_order) - Create a new order -* [delete_order](#delete_order) - Delete an order * [get_order](#get_order) - Get an order -* [list_orders](#list_orders) - List orders -* [update_order](#update_order) - Update an order ## create_order @@ -18,78 +14,48 @@ Creates a new order in the database. ### Example Usage + ```python -import sdk -import dateutil.parser -from sdk.models import shared - -s = sdk.SDK() - -req = shared.OrderCreate( - burger_ids=[ - 1, - 1, - 1, - ], - note='No onions.', - status=shared.OrderCreateStatus.PENDING, - table=1, - time=dateutil.parser.isoparse('2021-01-01T00:00:00.000Z'), -) - -res = s.orders.create_order(req) - -if res.create_order_201_application_json_object is not None: - # handle response -``` - -### Parameters - -| Parameter | Type | Required | Description | -| ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | -| `request` | [shared.OrderCreate](../../models/shared/ordercreate.md) | :heavy_check_mark: | The request object to use for the request. | -| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | +from openapi import SDK, models +from openapi.utils import parse_datetime -### Response - -**[operations.CreateOrderResponse](../../models/operations/createorderresponse.md)** - - -## delete_order - -Deletes an order from the database. - -### Example Usage - -```python -import sdk -from sdk.models import operations +with SDK() as sdk: -s = sdk.SDK() + res = sdk.orders.create_order(request={ + "burger_ids": [ + 1, + 2, + ], + "note": "No onions.", + "status": models.Status.PENDING, + "table": 1, + "time": parse_datetime("2021-01-01T00:00:00Z"), + }) -req = operations.DeleteOrderRequest( - id=1, -) + assert res is not None -res = s.orders.delete_order(req) + # Handle response + print(res) -if res.status_code == 200: - # handle response ``` ### Parameters -| Parameter | Type | Required | Description | -| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | -| `request` | [operations.DeleteOrderRequest](../../models/operations/deleteorderrequest.md) | :heavy_check_mark: | The request object to use for the request. | -| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | - +| Parameter | Type | Required | Description | +| ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | +| `request` | [models.OrderSchema](../../models/orderschema.md) | :heavy_check_mark: | The request object to use for the request. | +| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | ### Response -**[operations.DeleteOrderResponse](../../models/operations/deleteorderresponse.md)** +**[models.OrderSchemaOutput](../../models/orderschemaoutput.md)** + +### Errors +| Error Type | Status Code | Content Type | +| --------------- | --------------- | --------------- | +| errors.SDKError | 4XX, 5XX | \*/\* | ## get_order @@ -97,109 +63,37 @@ Gets an order from the database. ### Example Usage + ```python -import sdk -from sdk.models import operations +from openapi import SDK -s = sdk.SDK() -req = operations.GetOrderRequest( - id=1, -) +with SDK() as sdk: -res = s.orders.get_order(req) - -if res.get_order_200_application_json_object is not None: - # handle response -``` - -### Parameters - -| Parameter | Type | Required | Description | -| ------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------ | -| `request` | [operations.GetOrderRequest](../../models/operations/getorderrequest.md) | :heavy_check_mark: | The request object to use for the request. | -| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | - - -### Response + res = sdk.orders.get_order(request={ + "id": 1, + }) -**[operations.GetOrderResponse](../../models/operations/getorderresponse.md)** + assert res is not None + # Handle response + print(res) -## list_orders - -Lists all orders in the database. - -### Example Usage - -```python -import sdk - - -s = sdk.SDK() - - -res = s.orders.list_orders() - -if res.list_orders_200_application_json_objects is not None: - # handle response ``` ### Parameters | Parameter | Type | Required | Description | | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------- | +| `request` | [models.GetOrderRequest](../../models/getorderrequest.md) | :heavy_check_mark: | The request object to use for the request. | | `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | - ### Response -**[operations.ListOrdersResponse](../../models/operations/listordersresponse.md)** - - -## update_order - -Updates an order in the database. - -### Example Usage - -```python -import sdk -import dateutil.parser -from sdk.models import operations, shared - -s = sdk.SDK() - -req = operations.UpdateOrderRequest( - order_update=shared.OrderUpdate( - burger_ids=[ - 1, - 1, - 1, - ], - note='No onions.', - status=shared.OrderUpdateStatus.PENDING, - table=1, - time=dateutil.parser.isoparse('2021-01-01T00:00:00.000Z'), - ), - id=1, -) - -res = s.orders.update_order(req) - -if res.update_order_200_application_json_object is not None: - # handle response -``` - -### Parameters - -| Parameter | Type | Required | Description | -| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ | -| `request` | [operations.UpdateOrderRequest](../../models/operations/updateorderrequest.md) | :heavy_check_mark: | The request object to use for the request. | -| `retries` | [Optional[utils.RetryConfig]](../../models/utils/retryconfig.md) | :heavy_minus_sign: | Configuration to override the default retry behavior of the client. | - - -### Response +**[models.OrderSchemaOutput](../../models/orderschemaoutput.md)** -**[operations.UpdateOrderResponse](../../models/operations/updateorderresponse.md)** +### Errors +| Error Type | Status Code | Content Type | +| --------------- | --------------- | --------------- | +| errors.SDKError | 4XX, 5XX | \*/\* | \ No newline at end of file diff --git a/zod-openapi/sdk/gen.yaml b/zod-openapi/sdk/gen.yaml index 4952d1f..ded079b 100755 --- a/zod-openapi/sdk/gen.yaml +++ b/zod-openapi/sdk/gen.yaml @@ -1,16 +1,67 @@ -configVersion: 1.0.0 +configVersion: 2.0.0 generation: sdkClassName: SDK - singleTagPerOp: false -features: - python: - core: 2.82.0 - globalServerURLs: 2.81.1 - retries: 2.81.1 + usageSnippets: + optionalPropertyRendering: withExample + sdkInitStyle: constructor + fixes: + nameResolutionFeb2025: false + parameterOrderingFeb2024: false + requestResponseComponentNamesFeb2024: false + securityFeb2025: false + sharedErrorComponentsApr2025: false + auth: + oAuth2ClientCredentialsEnabled: false + oAuth2PasswordEnabled: false + hoistGlobalSecurity: true + schemas: + allOfMergeStrategy: shallowMerge + tests: + generateTests: true + generateNewTests: false + skipResponseBodyAssertions: false python: - version: 0.0.1 + version: 0.2.0 + additionalDependencies: + dev: {} + main: {} + allowedRedefinedBuiltins: + - id + - object + asyncMode: both author: Speakeasy + authors: + - Speakeasy + baseErrorName: SDKBaseError clientServerStatusCodesAsErrors: true + defaultErrorName: SDKError description: Python Client SDK Generated by Speakeasy + enableCustomCodeRegions: false + enumFormat: enum + fixFlags: + asyncPaginationSep2025: false + responseRequiredSep2024: false + flattenGlobalSecurity: true + flattenRequests: false + flatteningOrder: parameters-first + imports: + option: openapi + paths: + callbacks: "" + errors: errors + operations: "" + shared: "" + webhooks: "" + inputModelSuffix: input + legacyPyright: true maxMethodParams: 0 + methodArguments: infer-optional-args + moduleName: "" + outputModelSuffix: output + packageManager: poetry packageName: openapi + pytestFilterWarnings: [] + pytestTimeout: 0 + responseFormat: flat + sseFlatResponse: false + templateVersion: v2 diff --git a/zod-openapi/sdk/poetry.toml b/zod-openapi/sdk/poetry.toml new file mode 100644 index 0000000..cd3492a --- /dev/null +++ b/zod-openapi/sdk/poetry.toml @@ -0,0 +1,3 @@ + +[virtualenvs] +in-project = true diff --git a/zod-openapi/sdk/py.typed b/zod-openapi/sdk/py.typed new file mode 100644 index 0000000..3e38f1a --- /dev/null +++ b/zod-openapi/sdk/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561. The package enables type hints. diff --git a/zod-openapi/sdk/pylintrc b/zod-openapi/sdk/pylintrc index 21a3894..e8cd3e8 100755 --- a/zod-openapi/sdk/pylintrc +++ b/zod-openapi/sdk/pylintrc @@ -59,10 +59,11 @@ ignore-paths= # Emacs file locks ignore-patterns=^\.# -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis). It -# supports qualified module names, as well as Unix pattern matching. +# List of module names for which member attributes should not be checked and +# will not be imported (useful for modules/projects where namespaces are +# manipulated during runtime and thus existing member attributes cannot be +# deduced by static analysis). It supports qualified module names, as well as +# Unix pattern matching. ignored-modules= # Python code to execute, usually for sys.path manipulation such as @@ -93,6 +94,12 @@ py-version=3.9 # Discover python modules and packages in the file system subtree. recursive=no +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +source-roots=src + # When enabled, pylint would attempt to guess common misconfiguration and emit # user-friendly hints instead of false-positive error messages. suggestion-mode=yes @@ -116,12 +123,12 @@ argument-naming-style=snake_case #argument-rgx= # Naming style matching correct attribute names. -attr-naming-style=snake_case +#attr-naming-style=snake_case # Regular expression matching correct attribute names. Overrides attr-naming- # style. If left empty, attribute names will be checked with the set naming # style. -#attr-rgx= +attr-rgx=[^\W\d][^\W]*|__.*__$ # Bad variable names which should always be refused, separated by a comma. bad-names= @@ -180,6 +187,7 @@ good-names=i, ex, Run, _, + e, id # Good variable names regexes, separated by a comma. If names match any regex, @@ -224,6 +232,10 @@ no-docstring-rgx=^_ # These decorators are taken in consideration only for invalid-name. property-classes=abc.abstractproperty +# Regular expression matching correct type alias names. If left empty, type +# alias names will be checked with the set naming style. +typealias-rgx=.* + # Regular expression matching correct type variable names. If left empty, type # variable names will be checked with the set naming style. #typevar-rgx= @@ -246,15 +258,12 @@ check-protected-access-in-special-methods=no defining-attr-methods=__init__, __new__, setUp, + asyncSetUp, __post_init__ # List of member names, which should be excluded from the protected access # warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make +exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls @@ -417,6 +426,8 @@ disable=raw-checker-failed, suppressed-message, useless-suppression, deprecated-pragma, + use-implicit-booleaness-not-comparison-to-string, + use-implicit-booleaness-not-comparison-to-zero, use-symbolic-message-instead, trailing-whitespace, line-too-long, @@ -429,7 +440,6 @@ disable=raw-checker-failed, broad-exception-raised, too-few-public-methods, too-many-branches, - chained-comparison, duplicate-code, trailing-newlines, too-many-public-methods, @@ -440,13 +450,21 @@ disable=raw-checker-failed, cyclic-import, too-many-nested-blocks, too-many-boolean-expressions, - no-else-raise + no-else-raise, + bare-except, + broad-exception-caught, + fixme, + relative-beyond-top-level, + consider-using-with, + wildcard-import, + unused-wildcard-import, + too-many-return-statements # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option # multiple time (only on the command line, not in the configuration file where # it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member +enable= [METHOD_ARGS] @@ -492,8 +510,9 @@ evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor # used to format the message information. See doc for all details. msg-template= -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. +# Set the output format. Available formats are: text, parseable, colorized, +# json2 (improved json format), json (old json format) and msvs (visual +# studio). You can also give a reporter class, e.g. # mypackage.mymodule.MyReporterClass. #output-format= @@ -527,8 +546,8 @@ min-similarity-lines=4 # Limits count of emitted suggestions for spelling mistakes. max-spelling-suggestions=4 -# Spelling dictionary name. Available dictionaries: none. To make it work, -# install the 'python-enchant' package. +# Spelling dictionary name. No available dictionaries : You need to install +# both the python package and the system dependency for enchant to work. spelling-dict= # List of comma separated words that should be considered directives if they @@ -621,7 +640,7 @@ additional-builtins= allow-global-unused-variables=yes # List of names allowed to shadow builtins -allowed-redefined-builtins= +allowed-redefined-builtins=id,object # List of strings which can identify a callback function by name. A callback # name must start or end with one of those strings. @@ -640,4 +659,4 @@ init-import=no # List of qualified module names which can have objects that can redefine # builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io \ No newline at end of file diff --git a/zod-openapi/sdk/pyproject.toml b/zod-openapi/sdk/pyproject.toml new file mode 100644 index 0000000..d593b5c --- /dev/null +++ b/zod-openapi/sdk/pyproject.toml @@ -0,0 +1,56 @@ + +[project] +name = "openapi" +version = "0.2.0" +description = "Python Client SDK Generated by Speakeasy" +authors = [{ name = "Speakeasy" },] +readme = "README.md" +requires-python = ">=3.9.2" +dependencies = [ + "httpcore >=1.0.9", + "httpx >=0.28.1", + "pydantic >=2.11.2", +] + +[tool.poetry] +packages = [ + { include = "openapi", from = "src" } +] +include = ["py.typed", "src/openapi/py.typed"] + +[tool.setuptools.package-data] +"*" = ["py.typed", "src/openapi/py.typed"] + +[virtualenvs] +in-project = true + +[tool.poetry.group.dev.dependencies] +mypy = "==1.15.0" +pylint = "==3.2.3" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +asyncio_default_fixture_loop_scope = "function" +pythonpath = ["src"] + +[tool.mypy] +disable_error_code = "misc" +explicit_package_bases = true +mypy_path = "src" + +[[tool.mypy.overrides]] +module = "typing_inspect" +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "jsonpath" +ignore_missing_imports = true + +[tool.pyright] +venvPath = "." +venv = ".venv" + + diff --git a/zod-openapi/sdk/scripts/publish.sh b/zod-openapi/sdk/scripts/publish.sh new file mode 100644 index 0000000..5eb52a4 --- /dev/null +++ b/zod-openapi/sdk/scripts/publish.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +export POETRY_PYPI_TOKEN_PYPI=${PYPI_TOKEN} + +poetry publish --build --skip-existing diff --git a/zod-openapi/sdk/src/openapi/__init__.py b/zod-openapi/sdk/src/openapi/__init__.py new file mode 100644 index 0000000..833c68c --- /dev/null +++ b/zod-openapi/sdk/src/openapi/__init__.py @@ -0,0 +1,17 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from ._version import ( + __title__, + __version__, + __openapi_doc_version__, + __gen_version__, + __user_agent__, +) +from .sdk import * +from .sdkconfiguration import * + + +VERSION: str = __version__ +OPENAPI_DOC_VERSION = __openapi_doc_version__ +SPEAKEASY_GENERATOR_VERSION = __gen_version__ +USER_AGENT = __user_agent__ diff --git a/zod-openapi/sdk/src/openapi/_hooks/__init__.py b/zod-openapi/sdk/src/openapi/_hooks/__init__.py new file mode 100644 index 0000000..2ee66cd --- /dev/null +++ b/zod-openapi/sdk/src/openapi/_hooks/__init__.py @@ -0,0 +1,5 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from .sdkhooks import * +from .types import * +from .registration import * diff --git a/zod-openapi/sdk/src/openapi/_hooks/registration.py b/zod-openapi/sdk/src/openapi/_hooks/registration.py new file mode 100644 index 0000000..cab4778 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/_hooks/registration.py @@ -0,0 +1,13 @@ +from .types import Hooks + + +# This file is only ever generated once on the first generation and then is free to be modified. +# Any hooks you wish to add should be registered in the init_hooks function. Feel free to define them +# in this file or in separate files in the hooks folder. + + +def init_hooks(hooks: Hooks): + # pylint: disable=unused-argument + """Add hooks by calling hooks.register{sdk_init/before_request/after_success/after_error}Hook + with an instance of a hook that implements that specific Hook interface + Hooks are registered per SDK instance, and are valid for the lifetime of the SDK instance""" diff --git a/zod-openapi/sdk/src/openapi/_hooks/sdkhooks.py b/zod-openapi/sdk/src/openapi/_hooks/sdkhooks.py new file mode 100644 index 0000000..6e59cac --- /dev/null +++ b/zod-openapi/sdk/src/openapi/_hooks/sdkhooks.py @@ -0,0 +1,76 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +import httpx +from .types import ( + SDKInitHook, + BeforeRequestContext, + BeforeRequestHook, + AfterSuccessContext, + AfterSuccessHook, + AfterErrorContext, + AfterErrorHook, + Hooks, +) +from .registration import init_hooks +from typing import List, Optional, Tuple +from openapi.httpclient import HttpClient + + +class SDKHooks(Hooks): + def __init__(self) -> None: + self.sdk_init_hooks: List[SDKInitHook] = [] + self.before_request_hooks: List[BeforeRequestHook] = [] + self.after_success_hooks: List[AfterSuccessHook] = [] + self.after_error_hooks: List[AfterErrorHook] = [] + init_hooks(self) + + def register_sdk_init_hook(self, hook: SDKInitHook) -> None: + self.sdk_init_hooks.append(hook) + + def register_before_request_hook(self, hook: BeforeRequestHook) -> None: + self.before_request_hooks.append(hook) + + def register_after_success_hook(self, hook: AfterSuccessHook) -> None: + self.after_success_hooks.append(hook) + + def register_after_error_hook(self, hook: AfterErrorHook) -> None: + self.after_error_hooks.append(hook) + + def sdk_init(self, base_url: str, client: HttpClient) -> Tuple[str, HttpClient]: + for hook in self.sdk_init_hooks: + base_url, client = hook.sdk_init(base_url, client) + return base_url, client + + def before_request( + self, hook_ctx: BeforeRequestContext, request: httpx.Request + ) -> httpx.Request: + for hook in self.before_request_hooks: + out = hook.before_request(hook_ctx, request) + if isinstance(out, Exception): + raise out + request = out + + return request + + def after_success( + self, hook_ctx: AfterSuccessContext, response: httpx.Response + ) -> httpx.Response: + for hook in self.after_success_hooks: + out = hook.after_success(hook_ctx, response) + if isinstance(out, Exception): + raise out + response = out + return response + + def after_error( + self, + hook_ctx: AfterErrorContext, + response: Optional[httpx.Response], + error: Optional[Exception], + ) -> Tuple[Optional[httpx.Response], Optional[Exception]]: + for hook in self.after_error_hooks: + result = hook.after_error(hook_ctx, response, error) + if isinstance(result, Exception): + raise result + response, error = result + return response, error diff --git a/zod-openapi/sdk/src/openapi/_hooks/types.py b/zod-openapi/sdk/src/openapi/_hooks/types.py new file mode 100644 index 0000000..fb53205 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/_hooks/types.py @@ -0,0 +1,113 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from abc import ABC, abstractmethod +import httpx +from openapi.httpclient import HttpClient +from openapi.sdkconfiguration import SDKConfiguration +from typing import Any, Callable, List, Optional, Tuple, Union + + +class HookContext: + config: SDKConfiguration + base_url: str + operation_id: str + oauth2_scopes: Optional[List[str]] = None + security_source: Optional[Union[Any, Callable[[], Any]]] = None + + def __init__( + self, + config: SDKConfiguration, + base_url: str, + operation_id: str, + oauth2_scopes: Optional[List[str]], + security_source: Optional[Union[Any, Callable[[], Any]]], + ): + self.config = config + self.base_url = base_url + self.operation_id = operation_id + self.oauth2_scopes = oauth2_scopes + self.security_source = security_source + + +class BeforeRequestContext(HookContext): + def __init__(self, hook_ctx: HookContext): + super().__init__( + hook_ctx.config, + hook_ctx.base_url, + hook_ctx.operation_id, + hook_ctx.oauth2_scopes, + hook_ctx.security_source, + ) + + +class AfterSuccessContext(HookContext): + def __init__(self, hook_ctx: HookContext): + super().__init__( + hook_ctx.config, + hook_ctx.base_url, + hook_ctx.operation_id, + hook_ctx.oauth2_scopes, + hook_ctx.security_source, + ) + + +class AfterErrorContext(HookContext): + def __init__(self, hook_ctx: HookContext): + super().__init__( + hook_ctx.config, + hook_ctx.base_url, + hook_ctx.operation_id, + hook_ctx.oauth2_scopes, + hook_ctx.security_source, + ) + + +class SDKInitHook(ABC): + @abstractmethod + def sdk_init(self, base_url: str, client: HttpClient) -> Tuple[str, HttpClient]: + pass + + +class BeforeRequestHook(ABC): + @abstractmethod + def before_request( + self, hook_ctx: BeforeRequestContext, request: httpx.Request + ) -> Union[httpx.Request, Exception]: + pass + + +class AfterSuccessHook(ABC): + @abstractmethod + def after_success( + self, hook_ctx: AfterSuccessContext, response: httpx.Response + ) -> Union[httpx.Response, Exception]: + pass + + +class AfterErrorHook(ABC): + @abstractmethod + def after_error( + self, + hook_ctx: AfterErrorContext, + response: Optional[httpx.Response], + error: Optional[Exception], + ) -> Union[Tuple[Optional[httpx.Response], Optional[Exception]], Exception]: + pass + + +class Hooks(ABC): + @abstractmethod + def register_sdk_init_hook(self, hook: SDKInitHook): + pass + + @abstractmethod + def register_before_request_hook(self, hook: BeforeRequestHook): + pass + + @abstractmethod + def register_after_success_hook(self, hook: AfterSuccessHook): + pass + + @abstractmethod + def register_after_error_hook(self, hook: AfterErrorHook): + pass diff --git a/zod-openapi/sdk/src/openapi/_version.py b/zod-openapi/sdk/src/openapi/_version.py new file mode 100644 index 0000000..20b03e9 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/_version.py @@ -0,0 +1,15 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +import importlib.metadata + +__title__: str = "openapi" +__version__: str = "0.2.0" +__openapi_doc_version__: str = "1.0.0" +__gen_version__: str = "2.730.5" +__user_agent__: str = "speakeasy-sdk/python 0.2.0 2.730.5 1.0.0 openapi" + +try: + if __package__ is not None: + __version__ = importlib.metadata.version(__package__) +except importlib.metadata.PackageNotFoundError: + pass diff --git a/zod-openapi/sdk/src/openapi/basesdk.py b/zod-openapi/sdk/src/openapi/basesdk.py new file mode 100644 index 0000000..f30d30d --- /dev/null +++ b/zod-openapi/sdk/src/openapi/basesdk.py @@ -0,0 +1,360 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from .sdkconfiguration import SDKConfiguration +import httpx +from openapi import errors, utils +from openapi._hooks import AfterErrorContext, AfterSuccessContext, BeforeRequestContext +from openapi.utils import RetryConfig, SerializedRequestBody, get_body_content +from typing import Callable, List, Mapping, Optional, Tuple +from urllib.parse import parse_qs, urlparse + + +class BaseSDK: + sdk_configuration: SDKConfiguration + parent_ref: Optional[object] = None + """ + Reference to the root SDK instance, if any. This will prevent it from + being garbage collected while there are active streams. + """ + + def __init__( + self, + sdk_config: SDKConfiguration, + parent_ref: Optional[object] = None, + ) -> None: + self.sdk_configuration = sdk_config + self.parent_ref = parent_ref + + def _get_url(self, base_url, url_variables): + sdk_url, sdk_variables = self.sdk_configuration.get_server_details() + + if base_url is None: + base_url = sdk_url + + if url_variables is None: + url_variables = sdk_variables + + return utils.template_url(base_url, url_variables) + + def _build_request_async( + self, + method, + path, + base_url, + url_variables, + request, + request_body_required, + request_has_path_params, + request_has_query_params, + user_agent_header, + accept_header_value, + _globals=None, + security=None, + timeout_ms: Optional[int] = None, + get_serialized_body: Optional[ + Callable[[], Optional[SerializedRequestBody]] + ] = None, + url_override: Optional[str] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> httpx.Request: + client = self.sdk_configuration.async_client + return self._build_request_with_client( + client, + method, + path, + base_url, + url_variables, + request, + request_body_required, + request_has_path_params, + request_has_query_params, + user_agent_header, + accept_header_value, + _globals, + security, + timeout_ms, + get_serialized_body, + url_override, + http_headers, + ) + + def _build_request( + self, + method, + path, + base_url, + url_variables, + request, + request_body_required, + request_has_path_params, + request_has_query_params, + user_agent_header, + accept_header_value, + _globals=None, + security=None, + timeout_ms: Optional[int] = None, + get_serialized_body: Optional[ + Callable[[], Optional[SerializedRequestBody]] + ] = None, + url_override: Optional[str] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> httpx.Request: + client = self.sdk_configuration.client + return self._build_request_with_client( + client, + method, + path, + base_url, + url_variables, + request, + request_body_required, + request_has_path_params, + request_has_query_params, + user_agent_header, + accept_header_value, + _globals, + security, + timeout_ms, + get_serialized_body, + url_override, + http_headers, + ) + + def _build_request_with_client( + self, + client, + method, + path, + base_url, + url_variables, + request, + request_body_required, + request_has_path_params, + request_has_query_params, + user_agent_header, + accept_header_value, + _globals=None, + security=None, + timeout_ms: Optional[int] = None, + get_serialized_body: Optional[ + Callable[[], Optional[SerializedRequestBody]] + ] = None, + url_override: Optional[str] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> httpx.Request: + query_params = {} + + url = url_override + if url is None: + url = utils.generate_url( + self._get_url(base_url, url_variables), + path, + request if request_has_path_params else None, + _globals if request_has_path_params else None, + ) + + query_params = utils.get_query_params( + request if request_has_query_params else None, + _globals if request_has_query_params else None, + ) + else: + # Pick up the query parameter from the override so they can be + # preserved when building the request later on (necessary as of + # httpx 0.28). + parsed_override = urlparse(str(url_override)) + query_params = parse_qs(parsed_override.query, keep_blank_values=True) + + headers = utils.get_headers(request, _globals) + headers["Accept"] = accept_header_value + headers[user_agent_header] = self.sdk_configuration.user_agent + + if security is not None: + if callable(security): + security = security() + + if security is not None: + security_headers, security_query_params = utils.get_security(security) + headers = {**headers, **security_headers} + query_params = {**query_params, **security_query_params} + + serialized_request_body = SerializedRequestBody() + if get_serialized_body is not None: + rb = get_serialized_body() + if request_body_required and rb is None: + raise ValueError("request body is required") + + if rb is not None: + serialized_request_body = rb + + if ( + serialized_request_body.media_type is not None + and serialized_request_body.media_type + not in ( + "multipart/form-data", + "multipart/mixed", + ) + ): + headers["content-type"] = serialized_request_body.media_type + + if http_headers is not None: + for header, value in http_headers.items(): + headers[header] = value + + timeout = timeout_ms / 1000 if timeout_ms is not None else None + + return client.build_request( + method, + url, + params=query_params, + content=serialized_request_body.content, + data=serialized_request_body.data, + files=serialized_request_body.files, + headers=headers, + timeout=timeout, + ) + + def do_request( + self, + hook_ctx, + request, + error_status_codes, + stream=False, + retry_config: Optional[Tuple[RetryConfig, List[str]]] = None, + ) -> httpx.Response: + client = self.sdk_configuration.client + logger = self.sdk_configuration.debug_logger + + hooks = self.sdk_configuration.__dict__["_hooks"] + + def do(): + http_res = None + try: + req = hooks.before_request(BeforeRequestContext(hook_ctx), request) + logger.debug( + "Request:\nMethod: %s\nURL: %s\nHeaders: %s\nBody: %s", + req.method, + req.url, + req.headers, + get_body_content(req), + ) + + if client is None: + raise ValueError("client is required") + + http_res = client.send(req, stream=stream) + except Exception as e: + _, e = hooks.after_error(AfterErrorContext(hook_ctx), None, e) + if e is not None: + logger.debug("Request Exception", exc_info=True) + raise e + + if http_res is None: + logger.debug("Raising no response SDK error") + raise errors.NoResponseError("No response received") + + logger.debug( + "Response:\nStatus Code: %s\nURL: %s\nHeaders: %s\nBody: %s", + http_res.status_code, + http_res.url, + http_res.headers, + "" if stream else http_res.text, + ) + + if utils.match_status_codes(error_status_codes, http_res.status_code): + result, err = hooks.after_error( + AfterErrorContext(hook_ctx), http_res, None + ) + if err is not None: + logger.debug("Request Exception", exc_info=True) + raise err + if result is not None: + http_res = result + else: + logger.debug("Raising unexpected SDK error") + raise errors.SDKError("Unexpected error occurred", http_res) + + return http_res + + if retry_config is not None: + http_res = utils.retry(do, utils.Retries(retry_config[0], retry_config[1])) + else: + http_res = do() + + if not utils.match_status_codes(error_status_codes, http_res.status_code): + http_res = hooks.after_success(AfterSuccessContext(hook_ctx), http_res) + + return http_res + + async def do_request_async( + self, + hook_ctx, + request, + error_status_codes, + stream=False, + retry_config: Optional[Tuple[RetryConfig, List[str]]] = None, + ) -> httpx.Response: + client = self.sdk_configuration.async_client + logger = self.sdk_configuration.debug_logger + + hooks = self.sdk_configuration.__dict__["_hooks"] + + async def do(): + http_res = None + try: + req = hooks.before_request(BeforeRequestContext(hook_ctx), request) + logger.debug( + "Request:\nMethod: %s\nURL: %s\nHeaders: %s\nBody: %s", + req.method, + req.url, + req.headers, + get_body_content(req), + ) + + if client is None: + raise ValueError("client is required") + + http_res = await client.send(req, stream=stream) + except Exception as e: + _, e = hooks.after_error(AfterErrorContext(hook_ctx), None, e) + if e is not None: + logger.debug("Request Exception", exc_info=True) + raise e + + if http_res is None: + logger.debug("Raising no response SDK error") + raise errors.NoResponseError("No response received") + + logger.debug( + "Response:\nStatus Code: %s\nURL: %s\nHeaders: %s\nBody: %s", + http_res.status_code, + http_res.url, + http_res.headers, + "" if stream else http_res.text, + ) + + if utils.match_status_codes(error_status_codes, http_res.status_code): + result, err = hooks.after_error( + AfterErrorContext(hook_ctx), http_res, None + ) + if err is not None: + logger.debug("Request Exception", exc_info=True) + raise err + if result is not None: + http_res = result + else: + logger.debug("Raising unexpected SDK error") + raise errors.SDKError("Unexpected error occurred", http_res) + + return http_res + + if retry_config is not None: + http_res = await utils.retry_async( + do, utils.Retries(retry_config[0], retry_config[1]) + ) + else: + http_res = await do() + + if not utils.match_status_codes(error_status_codes, http_res.status_code): + http_res = hooks.after_success(AfterSuccessContext(hook_ctx), http_res) + + return http_res diff --git a/zod-openapi/sdk/src/openapi/burgers.py b/zod-openapi/sdk/src/openapi/burgers.py new file mode 100644 index 0000000..fd8dd73 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/burgers.py @@ -0,0 +1,522 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from .basesdk import BaseSDK +from openapi import errors, models, utils +from openapi._hooks import HookContext +from openapi.types import BaseModel, OptionalNullable, UNSET +from openapi.utils.unmarshal_json_response import unmarshal_json_response +from typing import List, Mapping, Optional, Union, cast + + +class Burgers(BaseSDK): + def create_burger( + self, + *, + request: Optional[ + Union[models.BurgerSchema, models.BurgerSchemaTypedDict] + ] = None, + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> Optional[models.BurgerSchemaOutput]: + r"""Create a new burger + + Creates a new burger in the database. + + :param request: The request object to send. + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + + if not isinstance(request, BaseModel): + request = utils.unmarshal(request, Optional[models.BurgerSchema]) + request = cast(Optional[models.BurgerSchema], request) + + req = self._build_request( + method="POST", + path="/burgers", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=False, + request_has_path_params=False, + request_has_query_params=False, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + get_serialized_body=lambda: utils.serialize_request_body( + request, False, True, "json", Optional[models.BurgerSchema] + ), + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + else: + retries = utils.RetryConfig( + "backoff", utils.BackoffStrategy(500, 60000, 1.5, 3600000), True + ) + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["5XX"]) + + http_res = self.do_request( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="createBurger", + oauth2_scopes=None, + security_source=None, + ), + request=req, + error_status_codes=["4XX", "5XX"], + retry_config=retry_config, + ) + + if utils.match_response(http_res, "201", "application/json"): + return unmarshal_json_response( + Optional[models.BurgerSchemaOutput], http_res + ) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.SDKError("API error occurred", http_res, http_res_text) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.SDKError("API error occurred", http_res, http_res_text) + + raise errors.SDKError("Unexpected response received", http_res) + + async def create_burger_async( + self, + *, + request: Optional[ + Union[models.BurgerSchema, models.BurgerSchemaTypedDict] + ] = None, + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> Optional[models.BurgerSchemaOutput]: + r"""Create a new burger + + Creates a new burger in the database. + + :param request: The request object to send. + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + + if not isinstance(request, BaseModel): + request = utils.unmarshal(request, Optional[models.BurgerSchema]) + request = cast(Optional[models.BurgerSchema], request) + + req = self._build_request_async( + method="POST", + path="/burgers", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=False, + request_has_path_params=False, + request_has_query_params=False, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + get_serialized_body=lambda: utils.serialize_request_body( + request, False, True, "json", Optional[models.BurgerSchema] + ), + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + else: + retries = utils.RetryConfig( + "backoff", utils.BackoffStrategy(500, 60000, 1.5, 3600000), True + ) + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["5XX"]) + + http_res = await self.do_request_async( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="createBurger", + oauth2_scopes=None, + security_source=None, + ), + request=req, + error_status_codes=["4XX", "5XX"], + retry_config=retry_config, + ) + + if utils.match_response(http_res, "201", "application/json"): + return unmarshal_json_response( + Optional[models.BurgerSchemaOutput], http_res + ) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.SDKError("API error occurred", http_res, http_res_text) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.SDKError("API error occurred", http_res, http_res_text) + + raise errors.SDKError("Unexpected response received", http_res) + + def get_burger( + self, + *, + request: Union[models.GetBurgerRequest, models.GetBurgerRequestTypedDict], + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> Optional[models.BurgerSchemaOutput]: + r"""Get a burger + + Gets a burger from the database. + + :param request: The request object to send. + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + + if not isinstance(request, BaseModel): + request = utils.unmarshal(request, models.GetBurgerRequest) + request = cast(models.GetBurgerRequest, request) + + req = self._build_request( + method="GET", + path="/burgers/{id}", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=False, + request_has_path_params=True, + request_has_query_params=False, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + else: + retries = utils.RetryConfig( + "backoff", utils.BackoffStrategy(500, 60000, 1.5, 3600000), True + ) + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["5XX"]) + + http_res = self.do_request( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="getBurger", + oauth2_scopes=None, + security_source=None, + ), + request=req, + error_status_codes=["4XX", "5XX"], + retry_config=retry_config, + ) + + if utils.match_response(http_res, "200", "application/json"): + return unmarshal_json_response( + Optional[models.BurgerSchemaOutput], http_res + ) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.SDKError("API error occurred", http_res, http_res_text) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.SDKError("API error occurred", http_res, http_res_text) + + raise errors.SDKError("Unexpected response received", http_res) + + async def get_burger_async( + self, + *, + request: Union[models.GetBurgerRequest, models.GetBurgerRequestTypedDict], + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> Optional[models.BurgerSchemaOutput]: + r"""Get a burger + + Gets a burger from the database. + + :param request: The request object to send. + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + + if not isinstance(request, BaseModel): + request = utils.unmarshal(request, models.GetBurgerRequest) + request = cast(models.GetBurgerRequest, request) + + req = self._build_request_async( + method="GET", + path="/burgers/{id}", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=False, + request_has_path_params=True, + request_has_query_params=False, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + else: + retries = utils.RetryConfig( + "backoff", utils.BackoffStrategy(500, 60000, 1.5, 3600000), True + ) + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["5XX"]) + + http_res = await self.do_request_async( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="getBurger", + oauth2_scopes=None, + security_source=None, + ), + request=req, + error_status_codes=["4XX", "5XX"], + retry_config=retry_config, + ) + + if utils.match_response(http_res, "200", "application/json"): + return unmarshal_json_response( + Optional[models.BurgerSchemaOutput], http_res + ) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.SDKError("API error occurred", http_res, http_res_text) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.SDKError("API error occurred", http_res, http_res_text) + + raise errors.SDKError("Unexpected response received", http_res) + + def list_burgers( + self, + *, + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> Optional[List[models.BurgerSchemaOutput]]: + r"""List burgers + + Lists all burgers in the database. + + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + req = self._build_request( + method="GET", + path="/burgers", + base_url=base_url, + url_variables=url_variables, + request=None, + request_body_required=False, + request_has_path_params=False, + request_has_query_params=False, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + else: + retries = utils.RetryConfig( + "backoff", utils.BackoffStrategy(500, 60000, 1.5, 3600000), True + ) + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["5XX"]) + + http_res = self.do_request( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="listBurgers", + oauth2_scopes=None, + security_source=None, + ), + request=req, + error_status_codes=["4XX", "5XX"], + retry_config=retry_config, + ) + + if utils.match_response(http_res, "200", "application/json"): + return unmarshal_json_response( + Optional[List[models.BurgerSchemaOutput]], http_res + ) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.SDKError("API error occurred", http_res, http_res_text) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.SDKError("API error occurred", http_res, http_res_text) + + raise errors.SDKError("Unexpected response received", http_res) + + async def list_burgers_async( + self, + *, + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> Optional[List[models.BurgerSchemaOutput]]: + r"""List burgers + + Lists all burgers in the database. + + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + req = self._build_request_async( + method="GET", + path="/burgers", + base_url=base_url, + url_variables=url_variables, + request=None, + request_body_required=False, + request_has_path_params=False, + request_has_query_params=False, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + else: + retries = utils.RetryConfig( + "backoff", utils.BackoffStrategy(500, 60000, 1.5, 3600000), True + ) + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["5XX"]) + + http_res = await self.do_request_async( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="listBurgers", + oauth2_scopes=None, + security_source=None, + ), + request=req, + error_status_codes=["4XX", "5XX"], + retry_config=retry_config, + ) + + if utils.match_response(http_res, "200", "application/json"): + return unmarshal_json_response( + Optional[List[models.BurgerSchemaOutput]], http_res + ) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.SDKError("API error occurred", http_res, http_res_text) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.SDKError("API error occurred", http_res, http_res_text) + + raise errors.SDKError("Unexpected response received", http_res) diff --git a/zod-openapi/sdk/src/openapi/errors/__init__.py b/zod-openapi/sdk/src/openapi/errors/__init__.py new file mode 100644 index 0000000..9f36120 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/errors/__init__.py @@ -0,0 +1,58 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from .sdkbaseerror import SDKBaseError +from typing import TYPE_CHECKING +from importlib import import_module +import builtins +import sys + +if TYPE_CHECKING: + from .no_response_error import NoResponseError + from .responsevalidationerror import ResponseValidationError + from .sdkerror import SDKError + +__all__ = ["NoResponseError", "ResponseValidationError", "SDKBaseError", "SDKError"] + +_dynamic_imports: dict[str, str] = { + "NoResponseError": ".no_response_error", + "ResponseValidationError": ".responsevalidationerror", + "SDKError": ".sdkerror", +} + + +def dynamic_import(modname, retries=3): + for attempt in range(retries): + try: + return import_module(modname, __package__) + except KeyError: + # Clear any half-initialized module and retry + sys.modules.pop(modname, None) + if attempt == retries - 1: + break + raise KeyError(f"Failed to import module '{modname}' after {retries} attempts") + + +def __getattr__(attr_name: str) -> object: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__} " + ) + + try: + module = dynamic_import(module_name) + result = getattr(module, attr_name) + return result + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = builtins.list(_dynamic_imports.keys()) + return builtins.sorted(lazy_attrs) diff --git a/zod-openapi/sdk/src/openapi/errors/no_response_error.py b/zod-openapi/sdk/src/openapi/errors/no_response_error.py new file mode 100644 index 0000000..1deab64 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/errors/no_response_error.py @@ -0,0 +1,17 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from dataclasses import dataclass + + +@dataclass(unsafe_hash=True) +class NoResponseError(Exception): + """Error raised when no HTTP response is received from the server.""" + + message: str + + def __init__(self, message: str = "No response received"): + object.__setattr__(self, "message", message) + super().__init__(message) + + def __str__(self): + return self.message diff --git a/zod-openapi/sdk/src/openapi/errors/responsevalidationerror.py b/zod-openapi/sdk/src/openapi/errors/responsevalidationerror.py new file mode 100644 index 0000000..15291ac --- /dev/null +++ b/zod-openapi/sdk/src/openapi/errors/responsevalidationerror.py @@ -0,0 +1,27 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +import httpx +from typing import Optional +from dataclasses import dataclass + +from openapi.errors import SDKBaseError + + +@dataclass(unsafe_hash=True) +class ResponseValidationError(SDKBaseError): + """Error raised when there is a type mismatch between the response data and the expected Pydantic model.""" + + def __init__( + self, + message: str, + raw_response: httpx.Response, + cause: Exception, + body: Optional[str] = None, + ): + message = f"{message}: {cause}" + super().__init__(message, raw_response, body) + + @property + def cause(self): + """Normally the Pydantic ValidationError""" + return self.__cause__ diff --git a/zod-openapi/sdk/src/openapi/errors/sdkbaseerror.py b/zod-openapi/sdk/src/openapi/errors/sdkbaseerror.py new file mode 100644 index 0000000..9519a2e --- /dev/null +++ b/zod-openapi/sdk/src/openapi/errors/sdkbaseerror.py @@ -0,0 +1,30 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +import httpx +from typing import Optional +from dataclasses import dataclass, field + + +@dataclass(unsafe_hash=True) +class SDKBaseError(Exception): + """The base class for all HTTP error responses.""" + + message: str + status_code: int + body: str + headers: httpx.Headers = field(hash=False) + raw_response: httpx.Response = field(hash=False) + + def __init__( + self, message: str, raw_response: httpx.Response, body: Optional[str] = None + ): + object.__setattr__(self, "message", message) + object.__setattr__(self, "status_code", raw_response.status_code) + object.__setattr__( + self, "body", body if body is not None else raw_response.text + ) + object.__setattr__(self, "headers", raw_response.headers) + object.__setattr__(self, "raw_response", raw_response) + + def __str__(self): + return self.message diff --git a/zod-openapi/sdk/src/openapi/errors/sdkerror.py b/zod-openapi/sdk/src/openapi/errors/sdkerror.py new file mode 100644 index 0000000..2785a59 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/errors/sdkerror.py @@ -0,0 +1,40 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +import httpx +from typing import Optional +from dataclasses import dataclass + +from openapi.errors import SDKBaseError + +MAX_MESSAGE_LEN = 10_000 + + +@dataclass(unsafe_hash=True) +class SDKError(SDKBaseError): + """The fallback error class if no more specific error class is matched.""" + + def __init__( + self, message: str, raw_response: httpx.Response, body: Optional[str] = None + ): + body_display = body or raw_response.text or '""' + + if message: + message += ": " + message += f"Status {raw_response.status_code}" + + headers = raw_response.headers + content_type = headers.get("content-type", '""') + if content_type != "application/json": + if " " in content_type: + content_type = f'"{content_type}"' + message += f" Content-Type {content_type}" + + if len(body_display) > MAX_MESSAGE_LEN: + truncated = body_display[:MAX_MESSAGE_LEN] + remaining = len(body_display) - MAX_MESSAGE_LEN + body_display = f"{truncated}...and {remaining} more chars" + + message += f". Body: {body_display}" + message = message.strip() + + super().__init__(message, raw_response, body) diff --git a/zod-openapi/sdk/src/openapi/httpclient.py b/zod-openapi/sdk/src/openapi/httpclient.py new file mode 100644 index 0000000..89560b5 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/httpclient.py @@ -0,0 +1,125 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +# pyright: reportReturnType = false +import asyncio +from typing_extensions import Protocol, runtime_checkable +import httpx +from typing import Any, Optional, Union + + +@runtime_checkable +class HttpClient(Protocol): + def send( + self, + request: httpx.Request, + *, + stream: bool = False, + auth: Union[ + httpx._types.AuthTypes, httpx._client.UseClientDefault, None + ] = httpx.USE_CLIENT_DEFAULT, + follow_redirects: Union[ + bool, httpx._client.UseClientDefault + ] = httpx.USE_CLIENT_DEFAULT, + ) -> httpx.Response: + pass + + def build_request( + self, + method: str, + url: httpx._types.URLTypes, + *, + content: Optional[httpx._types.RequestContent] = None, + data: Optional[httpx._types.RequestData] = None, + files: Optional[httpx._types.RequestFiles] = None, + json: Optional[Any] = None, + params: Optional[httpx._types.QueryParamTypes] = None, + headers: Optional[httpx._types.HeaderTypes] = None, + cookies: Optional[httpx._types.CookieTypes] = None, + timeout: Union[ + httpx._types.TimeoutTypes, httpx._client.UseClientDefault + ] = httpx.USE_CLIENT_DEFAULT, + extensions: Optional[httpx._types.RequestExtensions] = None, + ) -> httpx.Request: + pass + + def close(self) -> None: + pass + + +@runtime_checkable +class AsyncHttpClient(Protocol): + async def send( + self, + request: httpx.Request, + *, + stream: bool = False, + auth: Union[ + httpx._types.AuthTypes, httpx._client.UseClientDefault, None + ] = httpx.USE_CLIENT_DEFAULT, + follow_redirects: Union[ + bool, httpx._client.UseClientDefault + ] = httpx.USE_CLIENT_DEFAULT, + ) -> httpx.Response: + pass + + def build_request( + self, + method: str, + url: httpx._types.URLTypes, + *, + content: Optional[httpx._types.RequestContent] = None, + data: Optional[httpx._types.RequestData] = None, + files: Optional[httpx._types.RequestFiles] = None, + json: Optional[Any] = None, + params: Optional[httpx._types.QueryParamTypes] = None, + headers: Optional[httpx._types.HeaderTypes] = None, + cookies: Optional[httpx._types.CookieTypes] = None, + timeout: Union[ + httpx._types.TimeoutTypes, httpx._client.UseClientDefault + ] = httpx.USE_CLIENT_DEFAULT, + extensions: Optional[httpx._types.RequestExtensions] = None, + ) -> httpx.Request: + pass + + async def aclose(self) -> None: + pass + + +class ClientOwner(Protocol): + client: Union[HttpClient, None] + async_client: Union[AsyncHttpClient, None] + + +def close_clients( + owner: ClientOwner, + sync_client: Union[HttpClient, None], + sync_client_supplied: bool, + async_client: Union[AsyncHttpClient, None], + async_client_supplied: bool, +) -> None: + """ + A finalizer function that is meant to be used with weakref.finalize to close + httpx clients used by an SDK so that underlying resources can be garbage + collected. + """ + + # Unset the client/async_client properties so there are no more references + # to them from the owning SDK instance and they can be reaped. + owner.client = None + owner.async_client = None + if sync_client is not None and not sync_client_supplied: + try: + sync_client.close() + except Exception: + pass + + if async_client is not None and not async_client_supplied: + try: + loop = asyncio.get_running_loop() + asyncio.run_coroutine_threadsafe(async_client.aclose(), loop) + except RuntimeError: + try: + asyncio.run(async_client.aclose()) + except RuntimeError: + # best effort + pass diff --git a/zod-openapi/sdk/src/openapi/models/__init__.py b/zod-openapi/sdk/src/openapi/models/__init__.py new file mode 100644 index 0000000..bde1563 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/models/__init__.py @@ -0,0 +1,90 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from typing import TYPE_CHECKING +from importlib import import_module +import builtins +import sys + +if TYPE_CHECKING: + from .burgerschema import BurgerSchema, BurgerSchemaTypedDict + from .burgerschemaoutput import BurgerSchemaOutput, BurgerSchemaOutputTypedDict + from .getburgerop import GetBurgerRequest, GetBurgerRequestTypedDict + from .getorderop import GetOrderRequest, GetOrderRequestTypedDict + from .orderschema import OrderSchema, OrderSchemaTypedDict, Status + from .orderschemaoutput import ( + OrderSchemaOutput, + OrderSchemaOutputStatus, + OrderSchemaOutputTypedDict, + ) + +__all__ = [ + "BurgerSchema", + "BurgerSchemaOutput", + "BurgerSchemaOutputTypedDict", + "BurgerSchemaTypedDict", + "GetBurgerRequest", + "GetBurgerRequestTypedDict", + "GetOrderRequest", + "GetOrderRequestTypedDict", + "OrderSchema", + "OrderSchemaOutput", + "OrderSchemaOutputStatus", + "OrderSchemaOutputTypedDict", + "OrderSchemaTypedDict", + "Status", +] + +_dynamic_imports: dict[str, str] = { + "BurgerSchema": ".burgerschema", + "BurgerSchemaTypedDict": ".burgerschema", + "BurgerSchemaOutput": ".burgerschemaoutput", + "BurgerSchemaOutputTypedDict": ".burgerschemaoutput", + "GetBurgerRequest": ".getburgerop", + "GetBurgerRequestTypedDict": ".getburgerop", + "GetOrderRequest": ".getorderop", + "GetOrderRequestTypedDict": ".getorderop", + "OrderSchema": ".orderschema", + "OrderSchemaTypedDict": ".orderschema", + "Status": ".orderschema", + "OrderSchemaOutput": ".orderschemaoutput", + "OrderSchemaOutputStatus": ".orderschemaoutput", + "OrderSchemaOutputTypedDict": ".orderschemaoutput", +} + + +def dynamic_import(modname, retries=3): + for attempt in range(retries): + try: + return import_module(modname, __package__) + except KeyError: + # Clear any half-initialized module and retry + sys.modules.pop(modname, None) + if attempt == retries - 1: + break + raise KeyError(f"Failed to import module '{modname}' after {retries} attempts") + + +def __getattr__(attr_name: str) -> object: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"No {attr_name} found in _dynamic_imports for module name -> {__name__} " + ) + + try: + module = dynamic_import(module_name) + result = getattr(module, attr_name) + return result + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = builtins.list(_dynamic_imports.keys()) + return builtins.sorted(lazy_attrs) diff --git a/zod-openapi/sdk/src/openapi/models/burgerschema.py b/zod-openapi/sdk/src/openapi/models/burgerschema.py new file mode 100644 index 0000000..ed2a24d --- /dev/null +++ b/zod-openapi/sdk/src/openapi/models/burgerschema.py @@ -0,0 +1,25 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from openapi.types import BaseModel +from typing import Optional +from typing_extensions import NotRequired, TypedDict + + +class BurgerSchemaTypedDict(TypedDict): + r"""A burger served at the restaurant.""" + + name: str + r"""The name of the burger.""" + description: NotRequired[str] + r"""The description of the burger.""" + + +class BurgerSchema(BaseModel): + r"""A burger served at the restaurant.""" + + name: str + r"""The name of the burger.""" + + description: Optional[str] = None + r"""The description of the burger.""" diff --git a/zod-openapi/sdk/src/openapi/models/burgerschemaoutput.py b/zod-openapi/sdk/src/openapi/models/burgerschemaoutput.py new file mode 100644 index 0000000..ecafd79 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/models/burgerschemaoutput.py @@ -0,0 +1,30 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from openapi.types import BaseModel +from typing import Optional +from typing_extensions import NotRequired, TypedDict + + +class BurgerSchemaOutputTypedDict(TypedDict): + r"""A burger served at the restaurant.""" + + id: float + r"""The unique identifier of the burger.""" + name: str + r"""The name of the burger.""" + description: NotRequired[str] + r"""The description of the burger.""" + + +class BurgerSchemaOutput(BaseModel): + r"""A burger served at the restaurant.""" + + id: float + r"""The unique identifier of the burger.""" + + name: str + r"""The name of the burger.""" + + description: Optional[str] = None + r"""The description of the burger.""" diff --git a/zod-openapi/sdk/src/openapi/models/getburgerop.py b/zod-openapi/sdk/src/openapi/models/getburgerop.py new file mode 100644 index 0000000..93db5c6 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/models/getburgerop.py @@ -0,0 +1,18 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from openapi.types import BaseModel +from openapi.utils import FieldMetadata, PathParamMetadata +from typing_extensions import Annotated, TypedDict + + +class GetBurgerRequestTypedDict(TypedDict): + id: float + r"""The unique identifier of the burger.""" + + +class GetBurgerRequest(BaseModel): + id: Annotated[ + float, FieldMetadata(path=PathParamMetadata(style="simple", explode=False)) + ] + r"""The unique identifier of the burger.""" diff --git a/zod-openapi/sdk/src/openapi/models/getorderop.py b/zod-openapi/sdk/src/openapi/models/getorderop.py new file mode 100644 index 0000000..59917a3 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/models/getorderop.py @@ -0,0 +1,18 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from openapi.types import BaseModel +from openapi.utils import FieldMetadata, PathParamMetadata +from typing_extensions import Annotated, TypedDict + + +class GetOrderRequestTypedDict(TypedDict): + id: float + r"""The unique identifier of the order.""" + + +class GetOrderRequest(BaseModel): + id: Annotated[ + float, FieldMetadata(path=PathParamMetadata(style="simple", explode=False)) + ] + r"""The unique identifier of the order.""" diff --git a/zod-openapi/sdk/src/openapi/models/orderschema.py b/zod-openapi/sdk/src/openapi/models/orderschema.py new file mode 100644 index 0000000..d564516 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/models/orderschema.py @@ -0,0 +1,51 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from datetime import datetime +from enum import Enum +from openapi.types import BaseModel +from typing import List, Optional +from typing_extensions import NotRequired, TypedDict + + +class Status(str, Enum): + r"""The status of the order.""" + + PENDING = "pending" + IN_PROGRESS = "in_progress" + READY = "ready" + DELIVERED = "delivered" + + +class OrderSchemaTypedDict(TypedDict): + r"""An order placed at the restaurant.""" + + burger_ids: List[float] + r"""The burgers in the order.""" + status: Status + r"""The status of the order.""" + table: float + r"""The table the order is for.""" + time: datetime + r"""The time the order was placed.""" + note: NotRequired[str] + r"""A note for the order.""" + + +class OrderSchema(BaseModel): + r"""An order placed at the restaurant.""" + + burger_ids: List[float] + r"""The burgers in the order.""" + + status: Status + r"""The status of the order.""" + + table: float + r"""The table the order is for.""" + + time: datetime + r"""The time the order was placed.""" + + note: Optional[str] = None + r"""A note for the order.""" diff --git a/zod-openapi/sdk/src/openapi/models/orderschemaoutput.py b/zod-openapi/sdk/src/openapi/models/orderschemaoutput.py new file mode 100644 index 0000000..831c17b --- /dev/null +++ b/zod-openapi/sdk/src/openapi/models/orderschemaoutput.py @@ -0,0 +1,56 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from __future__ import annotations +from datetime import datetime +from enum import Enum +from openapi.types import BaseModel +from typing import List, Optional +from typing_extensions import NotRequired, TypedDict + + +class OrderSchemaOutputStatus(str, Enum): + r"""The status of the order.""" + + PENDING = "pending" + IN_PROGRESS = "in_progress" + READY = "ready" + DELIVERED = "delivered" + + +class OrderSchemaOutputTypedDict(TypedDict): + r"""An order placed at the restaurant.""" + + burger_ids: List[float] + r"""The burgers in the order.""" + id: float + r"""The unique identifier of the order.""" + status: OrderSchemaOutputStatus + r"""The status of the order.""" + table: float + r"""The table the order is for.""" + time: datetime + r"""The time the order was placed.""" + note: NotRequired[str] + r"""A note for the order.""" + + +class OrderSchemaOutput(BaseModel): + r"""An order placed at the restaurant.""" + + burger_ids: List[float] + r"""The burgers in the order.""" + + id: float + r"""The unique identifier of the order.""" + + status: OrderSchemaOutputStatus + r"""The status of the order.""" + + table: float + r"""The table the order is for.""" + + time: datetime + r"""The time the order was placed.""" + + note: Optional[str] = None + r"""A note for the order.""" diff --git a/zod-openapi/sdk/src/openapi/orders.py b/zod-openapi/sdk/src/openapi/orders.py new file mode 100644 index 0000000..d8e3aed --- /dev/null +++ b/zod-openapi/sdk/src/openapi/orders.py @@ -0,0 +1,356 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from .basesdk import BaseSDK +from openapi import errors, models, utils +from openapi._hooks import HookContext +from openapi.types import BaseModel, OptionalNullable, UNSET +from openapi.utils.unmarshal_json_response import unmarshal_json_response +from typing import Mapping, Optional, Union, cast + + +class Orders(BaseSDK): + def create_order( + self, + *, + request: Optional[ + Union[models.OrderSchema, models.OrderSchemaTypedDict] + ] = None, + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> Optional[models.OrderSchemaOutput]: + r"""Create a new order + + Creates a new order in the database. + + :param request: The request object to send. + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + + if not isinstance(request, BaseModel): + request = utils.unmarshal(request, Optional[models.OrderSchema]) + request = cast(Optional[models.OrderSchema], request) + + req = self._build_request( + method="POST", + path="/orders", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=False, + request_has_path_params=False, + request_has_query_params=False, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + get_serialized_body=lambda: utils.serialize_request_body( + request, False, True, "json", Optional[models.OrderSchema] + ), + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + else: + retries = utils.RetryConfig( + "backoff", utils.BackoffStrategy(500, 60000, 1.5, 3600000), True + ) + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["5XX"]) + + http_res = self.do_request( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="createOrder", + oauth2_scopes=None, + security_source=None, + ), + request=req, + error_status_codes=["4XX", "5XX"], + retry_config=retry_config, + ) + + if utils.match_response(http_res, "201", "application/json"): + return unmarshal_json_response(Optional[models.OrderSchemaOutput], http_res) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.SDKError("API error occurred", http_res, http_res_text) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.SDKError("API error occurred", http_res, http_res_text) + + raise errors.SDKError("Unexpected response received", http_res) + + async def create_order_async( + self, + *, + request: Optional[ + Union[models.OrderSchema, models.OrderSchemaTypedDict] + ] = None, + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> Optional[models.OrderSchemaOutput]: + r"""Create a new order + + Creates a new order in the database. + + :param request: The request object to send. + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + + if not isinstance(request, BaseModel): + request = utils.unmarshal(request, Optional[models.OrderSchema]) + request = cast(Optional[models.OrderSchema], request) + + req = self._build_request_async( + method="POST", + path="/orders", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=False, + request_has_path_params=False, + request_has_query_params=False, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + get_serialized_body=lambda: utils.serialize_request_body( + request, False, True, "json", Optional[models.OrderSchema] + ), + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + else: + retries = utils.RetryConfig( + "backoff", utils.BackoffStrategy(500, 60000, 1.5, 3600000), True + ) + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["5XX"]) + + http_res = await self.do_request_async( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="createOrder", + oauth2_scopes=None, + security_source=None, + ), + request=req, + error_status_codes=["4XX", "5XX"], + retry_config=retry_config, + ) + + if utils.match_response(http_res, "201", "application/json"): + return unmarshal_json_response(Optional[models.OrderSchemaOutput], http_res) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.SDKError("API error occurred", http_res, http_res_text) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.SDKError("API error occurred", http_res, http_res_text) + + raise errors.SDKError("Unexpected response received", http_res) + + def get_order( + self, + *, + request: Union[models.GetOrderRequest, models.GetOrderRequestTypedDict], + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> Optional[models.OrderSchemaOutput]: + r"""Get an order + + Gets an order from the database. + + :param request: The request object to send. + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + + if not isinstance(request, BaseModel): + request = utils.unmarshal(request, models.GetOrderRequest) + request = cast(models.GetOrderRequest, request) + + req = self._build_request( + method="GET", + path="/orders/{id}", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=False, + request_has_path_params=True, + request_has_query_params=False, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + else: + retries = utils.RetryConfig( + "backoff", utils.BackoffStrategy(500, 60000, 1.5, 3600000), True + ) + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["5XX"]) + + http_res = self.do_request( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="getOrder", + oauth2_scopes=None, + security_source=None, + ), + request=req, + error_status_codes=["4XX", "5XX"], + retry_config=retry_config, + ) + + if utils.match_response(http_res, "200", "application/json"): + return unmarshal_json_response(Optional[models.OrderSchemaOutput], http_res) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.SDKError("API error occurred", http_res, http_res_text) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = utils.stream_to_text(http_res) + raise errors.SDKError("API error occurred", http_res, http_res_text) + + raise errors.SDKError("Unexpected response received", http_res) + + async def get_order_async( + self, + *, + request: Union[models.GetOrderRequest, models.GetOrderRequestTypedDict], + retries: OptionalNullable[utils.RetryConfig] = UNSET, + server_url: Optional[str] = None, + timeout_ms: Optional[int] = None, + http_headers: Optional[Mapping[str, str]] = None, + ) -> Optional[models.OrderSchemaOutput]: + r"""Get an order + + Gets an order from the database. + + :param request: The request object to send. + :param retries: Override the default retry configuration for this method + :param server_url: Override the default server URL for this method + :param timeout_ms: Override the default request timeout configuration for this method in milliseconds + :param http_headers: Additional headers to set or replace on requests. + """ + base_url = None + url_variables = None + if timeout_ms is None: + timeout_ms = self.sdk_configuration.timeout_ms + + if server_url is not None: + base_url = server_url + else: + base_url = self._get_url(base_url, url_variables) + + if not isinstance(request, BaseModel): + request = utils.unmarshal(request, models.GetOrderRequest) + request = cast(models.GetOrderRequest, request) + + req = self._build_request_async( + method="GET", + path="/orders/{id}", + base_url=base_url, + url_variables=url_variables, + request=request, + request_body_required=False, + request_has_path_params=True, + request_has_query_params=False, + user_agent_header="user-agent", + accept_header_value="application/json", + http_headers=http_headers, + timeout_ms=timeout_ms, + ) + + if retries == UNSET: + if self.sdk_configuration.retry_config is not UNSET: + retries = self.sdk_configuration.retry_config + else: + retries = utils.RetryConfig( + "backoff", utils.BackoffStrategy(500, 60000, 1.5, 3600000), True + ) + + retry_config = None + if isinstance(retries, utils.RetryConfig): + retry_config = (retries, ["5XX"]) + + http_res = await self.do_request_async( + hook_ctx=HookContext( + config=self.sdk_configuration, + base_url=base_url or "", + operation_id="getOrder", + oauth2_scopes=None, + security_source=None, + ), + request=req, + error_status_codes=["4XX", "5XX"], + retry_config=retry_config, + ) + + if utils.match_response(http_res, "200", "application/json"): + return unmarshal_json_response(Optional[models.OrderSchemaOutput], http_res) + if utils.match_response(http_res, "4XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.SDKError("API error occurred", http_res, http_res_text) + if utils.match_response(http_res, "5XX", "*"): + http_res_text = await utils.stream_to_text_async(http_res) + raise errors.SDKError("API error occurred", http_res, http_res_text) + + raise errors.SDKError("Unexpected response received", http_res) diff --git a/zod-openapi/sdk/src/openapi/py.typed b/zod-openapi/sdk/src/openapi/py.typed new file mode 100644 index 0000000..3e38f1a --- /dev/null +++ b/zod-openapi/sdk/src/openapi/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561. The package enables type hints. diff --git a/zod-openapi/sdk/src/openapi/sdk.py b/zod-openapi/sdk/src/openapi/sdk.py new file mode 100644 index 0000000..f830b39 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/sdk.py @@ -0,0 +1,174 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from .basesdk import BaseSDK +from .httpclient import AsyncHttpClient, ClientOwner, HttpClient, close_clients +from .sdkconfiguration import SDKConfiguration +from .utils.logger import Logger, get_default_logger +from .utils.retries import RetryConfig +import httpx +import importlib +from openapi import utils +from openapi._hooks import SDKHooks +from openapi.types import OptionalNullable, UNSET +import sys +from typing import Dict, Optional, TYPE_CHECKING, cast +import weakref + +if TYPE_CHECKING: + from openapi.burgers import Burgers + from openapi.orders import Orders + + +class SDK(BaseSDK): + r"""Burger Restaurant API: An API for managing burgers and orders at a restaurant.""" + + burgers: "Burgers" + orders: "Orders" + _sub_sdk_map = { + "burgers": ("openapi.burgers", "Burgers"), + "orders": ("openapi.orders", "Orders"), + } + + def __init__( + self, + server_idx: Optional[int] = None, + server_url: Optional[str] = None, + url_params: Optional[Dict[str, str]] = None, + client: Optional[HttpClient] = None, + async_client: Optional[AsyncHttpClient] = None, + retry_config: OptionalNullable[RetryConfig] = UNSET, + timeout_ms: Optional[int] = None, + debug_logger: Optional[Logger] = None, + ) -> None: + r"""Instantiates the SDK configuring it with the provided parameters. + + :param server_idx: The index of the server to use for all methods + :param server_url: The server URL to use for all methods + :param url_params: Parameters to optionally template the server URL with + :param client: The HTTP client to use for all synchronous methods + :param async_client: The Async HTTP client to use for all asynchronous methods + :param retry_config: The retry configuration to use for all supported methods + :param timeout_ms: Optional request timeout applied to each operation in milliseconds + """ + client_supplied = True + if client is None: + client = httpx.Client(follow_redirects=True) + client_supplied = False + + assert issubclass( + type(client), HttpClient + ), "The provided client must implement the HttpClient protocol." + + async_client_supplied = True + if async_client is None: + async_client = httpx.AsyncClient(follow_redirects=True) + async_client_supplied = False + + if debug_logger is None: + debug_logger = get_default_logger() + + assert issubclass( + type(async_client), AsyncHttpClient + ), "The provided async_client must implement the AsyncHttpClient protocol." + + if server_url is not None: + if url_params is not None: + server_url = utils.template_url(server_url, url_params) + + BaseSDK.__init__( + self, + SDKConfiguration( + client=client, + client_supplied=client_supplied, + async_client=async_client, + async_client_supplied=async_client_supplied, + server_url=server_url, + server_idx=server_idx, + retry_config=retry_config, + timeout_ms=timeout_ms, + debug_logger=debug_logger, + ), + parent_ref=self, + ) + + hooks = SDKHooks() + + # pylint: disable=protected-access + self.sdk_configuration.__dict__["_hooks"] = hooks + + current_server_url, *_ = self.sdk_configuration.get_server_details() + server_url, self.sdk_configuration.client = hooks.sdk_init( + current_server_url, client + ) + if current_server_url != server_url: + self.sdk_configuration.server_url = server_url + + weakref.finalize( + self, + close_clients, + cast(ClientOwner, self.sdk_configuration), + self.sdk_configuration.client, + self.sdk_configuration.client_supplied, + self.sdk_configuration.async_client, + self.sdk_configuration.async_client_supplied, + ) + + def dynamic_import(self, modname, retries=3): + for attempt in range(retries): + try: + return importlib.import_module(modname) + except KeyError: + # Clear any half-initialized module and retry + sys.modules.pop(modname, None) + if attempt == retries - 1: + break + raise KeyError(f"Failed to import module '{modname}' after {retries} attempts") + + def __getattr__(self, name: str): + if name in self._sub_sdk_map: + module_path, class_name = self._sub_sdk_map[name] + try: + module = self.dynamic_import(module_path) + klass = getattr(module, class_name) + instance = klass(self.sdk_configuration, parent_ref=self) + setattr(self, name, instance) + return instance + except ImportError as e: + raise AttributeError( + f"Failed to import module {module_path} for attribute {name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to find class {class_name} in module {module_path} for attribute {name}: {e}" + ) from e + + raise AttributeError( + f"'{type(self).__name__}' object has no attribute '{name}'" + ) + + def __dir__(self): + default_attrs = list(super().__dir__()) + lazy_attrs = list(self._sub_sdk_map.keys()) + return sorted(list(set(default_attrs + lazy_attrs))) + + def __enter__(self): + return self + + async def __aenter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if ( + self.sdk_configuration.client is not None + and not self.sdk_configuration.client_supplied + ): + self.sdk_configuration.client.close() + self.sdk_configuration.client = None + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if ( + self.sdk_configuration.async_client is not None + and not self.sdk_configuration.async_client_supplied + ): + await self.sdk_configuration.async_client.aclose() + self.sdk_configuration.async_client = None diff --git a/zod-openapi/sdk/src/openapi/sdkconfiguration.py b/zod-openapi/sdk/src/openapi/sdkconfiguration.py new file mode 100644 index 0000000..df2d132 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/sdkconfiguration.py @@ -0,0 +1,47 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from ._version import ( + __gen_version__, + __openapi_doc_version__, + __user_agent__, + __version__, +) +from .httpclient import AsyncHttpClient, HttpClient +from .utils import Logger, RetryConfig, remove_suffix +from dataclasses import dataclass +from openapi.types import OptionalNullable, UNSET +from pydantic import Field +from typing import Dict, Optional, Tuple, Union + + +SERVERS = [ + "https://example.com", + # The production server. +] +"""Contains the list of servers available to the SDK""" + + +@dataclass +class SDKConfiguration: + client: Union[HttpClient, None] + client_supplied: bool + async_client: Union[AsyncHttpClient, None] + async_client_supplied: bool + debug_logger: Logger + server_url: Optional[str] = "" + server_idx: Optional[int] = 0 + language: str = "python" + openapi_doc_version: str = __openapi_doc_version__ + sdk_version: str = __version__ + gen_version: str = __gen_version__ + user_agent: str = __user_agent__ + retry_config: OptionalNullable[RetryConfig] = Field(default_factory=lambda: UNSET) + timeout_ms: Optional[int] = None + + def get_server_details(self) -> Tuple[str, Dict[str, str]]: + if self.server_url is not None and self.server_url: + return remove_suffix(self.server_url, "/"), {} + if self.server_idx is None: + self.server_idx = 0 + + return SERVERS[self.server_idx], {} diff --git a/zod-openapi/sdk/src/openapi/types/__init__.py b/zod-openapi/sdk/src/openapi/types/__init__.py new file mode 100644 index 0000000..fc76fe0 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/types/__init__.py @@ -0,0 +1,21 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from .basemodel import ( + BaseModel, + Nullable, + OptionalNullable, + UnrecognizedInt, + UnrecognizedStr, + UNSET, + UNSET_SENTINEL, +) + +__all__ = [ + "BaseModel", + "Nullable", + "OptionalNullable", + "UnrecognizedInt", + "UnrecognizedStr", + "UNSET", + "UNSET_SENTINEL", +] diff --git a/zod-openapi/sdk/src/openapi/types/basemodel.py b/zod-openapi/sdk/src/openapi/types/basemodel.py new file mode 100644 index 0000000..231c2e3 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/types/basemodel.py @@ -0,0 +1,39 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from pydantic import ConfigDict, model_serializer +from pydantic import BaseModel as PydanticBaseModel +from typing import TYPE_CHECKING, Literal, Optional, TypeVar, Union +from typing_extensions import TypeAliasType, TypeAlias + + +class BaseModel(PydanticBaseModel): + model_config = ConfigDict( + populate_by_name=True, arbitrary_types_allowed=True, protected_namespaces=() + ) + + +class Unset(BaseModel): + @model_serializer(mode="plain") + def serialize_model(self): + return UNSET_SENTINEL + + def __bool__(self) -> Literal[False]: + return False + + +UNSET = Unset() +UNSET_SENTINEL = "~?~unset~?~sentinel~?~" + + +T = TypeVar("T") +if TYPE_CHECKING: + Nullable: TypeAlias = Union[T, None] + OptionalNullable: TypeAlias = Union[Optional[Nullable[T]], Unset] +else: + Nullable = TypeAliasType("Nullable", Union[T, None], type_params=(T,)) + OptionalNullable = TypeAliasType( + "OptionalNullable", Union[Optional[Nullable[T]], Unset], type_params=(T,) + ) + +UnrecognizedInt: TypeAlias = int +UnrecognizedStr: TypeAlias = str diff --git a/zod-openapi/sdk/src/openapi/utils/__init__.py b/zod-openapi/sdk/src/openapi/utils/__init__.py new file mode 100644 index 0000000..56164cf --- /dev/null +++ b/zod-openapi/sdk/src/openapi/utils/__init__.py @@ -0,0 +1,197 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from typing import TYPE_CHECKING +from importlib import import_module +import builtins +import sys + +if TYPE_CHECKING: + from .annotations import get_discriminator + from .datetimes import parse_datetime + from .enums import OpenEnumMeta + from .headers import get_headers, get_response_headers + from .metadata import ( + FieldMetadata, + find_metadata, + FormMetadata, + HeaderMetadata, + MultipartFormMetadata, + PathParamMetadata, + QueryParamMetadata, + RequestMetadata, + SecurityMetadata, + ) + from .queryparams import get_query_params + from .retries import BackoffStrategy, Retries, retry, retry_async, RetryConfig + from .requestbodies import serialize_request_body, SerializedRequestBody + from .security import get_security + from .serializers import ( + get_pydantic_model, + marshal_json, + unmarshal, + unmarshal_json, + serialize_decimal, + serialize_float, + serialize_int, + stream_to_text, + stream_to_text_async, + stream_to_bytes, + stream_to_bytes_async, + validate_const, + validate_decimal, + validate_float, + validate_int, + validate_open_enum, + ) + from .url import generate_url, template_url, remove_suffix + from .values import ( + get_global_from_env, + match_content_type, + match_status_codes, + match_response, + cast_partial, + ) + from .logger import Logger, get_body_content, get_default_logger + +__all__ = [ + "BackoffStrategy", + "FieldMetadata", + "find_metadata", + "FormMetadata", + "generate_url", + "get_body_content", + "get_default_logger", + "get_discriminator", + "parse_datetime", + "get_global_from_env", + "get_headers", + "get_pydantic_model", + "get_query_params", + "get_response_headers", + "get_security", + "HeaderMetadata", + "Logger", + "marshal_json", + "match_content_type", + "match_status_codes", + "match_response", + "MultipartFormMetadata", + "OpenEnumMeta", + "PathParamMetadata", + "QueryParamMetadata", + "remove_suffix", + "Retries", + "retry", + "retry_async", + "RetryConfig", + "RequestMetadata", + "SecurityMetadata", + "serialize_decimal", + "serialize_float", + "serialize_int", + "serialize_request_body", + "SerializedRequestBody", + "stream_to_text", + "stream_to_text_async", + "stream_to_bytes", + "stream_to_bytes_async", + "template_url", + "unmarshal", + "unmarshal_json", + "validate_decimal", + "validate_const", + "validate_float", + "validate_int", + "validate_open_enum", + "cast_partial", +] + +_dynamic_imports: dict[str, str] = { + "BackoffStrategy": ".retries", + "FieldMetadata": ".metadata", + "find_metadata": ".metadata", + "FormMetadata": ".metadata", + "generate_url": ".url", + "get_body_content": ".logger", + "get_default_logger": ".logger", + "get_discriminator": ".annotations", + "parse_datetime": ".datetimes", + "get_global_from_env": ".values", + "get_headers": ".headers", + "get_pydantic_model": ".serializers", + "get_query_params": ".queryparams", + "get_response_headers": ".headers", + "get_security": ".security", + "HeaderMetadata": ".metadata", + "Logger": ".logger", + "marshal_json": ".serializers", + "match_content_type": ".values", + "match_status_codes": ".values", + "match_response": ".values", + "MultipartFormMetadata": ".metadata", + "OpenEnumMeta": ".enums", + "PathParamMetadata": ".metadata", + "QueryParamMetadata": ".metadata", + "remove_suffix": ".url", + "Retries": ".retries", + "retry": ".retries", + "retry_async": ".retries", + "RetryConfig": ".retries", + "RequestMetadata": ".metadata", + "SecurityMetadata": ".metadata", + "serialize_decimal": ".serializers", + "serialize_float": ".serializers", + "serialize_int": ".serializers", + "serialize_request_body": ".requestbodies", + "SerializedRequestBody": ".requestbodies", + "stream_to_text": ".serializers", + "stream_to_text_async": ".serializers", + "stream_to_bytes": ".serializers", + "stream_to_bytes_async": ".serializers", + "template_url": ".url", + "unmarshal": ".serializers", + "unmarshal_json": ".serializers", + "validate_decimal": ".serializers", + "validate_const": ".serializers", + "validate_float": ".serializers", + "validate_int": ".serializers", + "validate_open_enum": ".serializers", + "cast_partial": ".values", +} + + +def dynamic_import(modname, retries=3): + for attempt in range(retries): + try: + return import_module(modname, __package__) + except KeyError: + # Clear any half-initialized module and retry + sys.modules.pop(modname, None) + if attempt == retries - 1: + break + raise KeyError(f"Failed to import module '{modname}' after {retries} attempts") + + +def __getattr__(attr_name: str) -> object: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError( + f"no {attr_name} found in _dynamic_imports, module name -> {__name__} " + ) + + try: + module = dynamic_import(module_name) + return getattr(module, attr_name) + except ImportError as e: + raise ImportError( + f"Failed to import {attr_name} from {module_name}: {e}" + ) from e + except AttributeError as e: + raise AttributeError( + f"Failed to get {attr_name} from {module_name}: {e}" + ) from e + + +def __dir__(): + lazy_attrs = builtins.list(_dynamic_imports.keys()) + return builtins.sorted(lazy_attrs) diff --git a/zod-openapi/sdk/src/openapi/utils/annotations.py b/zod-openapi/sdk/src/openapi/utils/annotations.py new file mode 100644 index 0000000..12e0aa4 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/utils/annotations.py @@ -0,0 +1,79 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from enum import Enum +from typing import Any, Optional + + +def get_discriminator(model: Any, fieldname: str, key: str) -> str: + """ + Recursively search for the discriminator attribute in a model. + + Args: + model (Any): The model to search within. + fieldname (str): The name of the field to search for. + key (str): The key to search for in dictionaries. + + Returns: + str: The name of the discriminator attribute. + + Raises: + ValueError: If the discriminator attribute is not found. + """ + upper_fieldname = fieldname.upper() + + def get_field_discriminator(field: Any) -> Optional[str]: + """Search for the discriminator attribute in a given field.""" + + if isinstance(field, dict): + if key in field: + return f"{field[key]}" + + if hasattr(field, fieldname): + attr = getattr(field, fieldname) + if isinstance(attr, Enum): + return f"{attr.value}" + return f"{attr}" + + if hasattr(field, upper_fieldname): + attr = getattr(field, upper_fieldname) + if isinstance(attr, Enum): + return f"{attr.value}" + return f"{attr}" + + return None + + def search_nested_discriminator(obj: Any) -> Optional[str]: + """Recursively search for discriminator in nested structures.""" + # First try direct field lookup + discriminator = get_field_discriminator(obj) + if discriminator is not None: + return discriminator + + # If it's a dict, search in nested values + if isinstance(obj, dict): + for value in obj.values(): + if isinstance(value, list): + # Search in list items + for item in value: + nested_discriminator = search_nested_discriminator(item) + if nested_discriminator is not None: + return nested_discriminator + elif isinstance(value, dict): + # Search in nested dict + nested_discriminator = search_nested_discriminator(value) + if nested_discriminator is not None: + return nested_discriminator + + return None + + if isinstance(model, list): + for field in model: + discriminator = search_nested_discriminator(field) + if discriminator is not None: + return discriminator + + discriminator = search_nested_discriminator(model) + if discriminator is not None: + return discriminator + + raise ValueError(f"Could not find discriminator field {fieldname} in {model}") diff --git a/zod-openapi/sdk/src/openapi/utils/datetimes.py b/zod-openapi/sdk/src/openapi/utils/datetimes.py new file mode 100644 index 0000000..a6c52cd --- /dev/null +++ b/zod-openapi/sdk/src/openapi/utils/datetimes.py @@ -0,0 +1,23 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from datetime import datetime +import sys + + +def parse_datetime(datetime_string: str) -> datetime: + """ + Convert a RFC 3339 / ISO 8601 formatted string into a datetime object. + Python versions 3.11 and later support parsing RFC 3339 directly with + datetime.fromisoformat(), but for earlier versions, this function + encapsulates the necessary extra logic. + """ + # Python 3.11 and later can parse RFC 3339 directly + if sys.version_info >= (3, 11): + return datetime.fromisoformat(datetime_string) + + # For Python 3.10 and earlier, a common ValueError is trailing 'Z' suffix, + # so fix that upfront. + if datetime_string.endswith("Z"): + datetime_string = datetime_string[:-1] + "+00:00" + + return datetime.fromisoformat(datetime_string) diff --git a/zod-openapi/sdk/src/openapi/utils/enums.py b/zod-openapi/sdk/src/openapi/utils/enums.py new file mode 100644 index 0000000..c3bc13c --- /dev/null +++ b/zod-openapi/sdk/src/openapi/utils/enums.py @@ -0,0 +1,74 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +import enum +import sys + +class OpenEnumMeta(enum.EnumMeta): + # The __call__ method `boundary` kwarg was added in 3.11 and must be present + # for pyright. Refer also: https://github.com/pylint-dev/pylint/issues/9622 + # pylint: disable=unexpected-keyword-arg + # The __call__ method `values` varg must be named for pyright. + # pylint: disable=keyword-arg-before-vararg + + if sys.version_info >= (3, 11): + def __call__( + cls, value, names=None, *values, module=None, qualname=None, type=None, start=1, boundary=None + ): + # The `type` kwarg also happens to be a built-in that pylint flags as + # redeclared. Safe to ignore this lint rule with this scope. + # pylint: disable=redefined-builtin + + if names is not None: + return super().__call__( + value, + names=names, + *values, + module=module, + qualname=qualname, + type=type, + start=start, + boundary=boundary, + ) + + try: + return super().__call__( + value, + names=names, # pyright: ignore[reportArgumentType] + *values, + module=module, + qualname=qualname, + type=type, + start=start, + boundary=boundary, + ) + except ValueError: + return value + else: + def __call__( + cls, value, names=None, *, module=None, qualname=None, type=None, start=1 + ): + # The `type` kwarg also happens to be a built-in that pylint flags as + # redeclared. Safe to ignore this lint rule with this scope. + # pylint: disable=redefined-builtin + + if names is not None: + return super().__call__( + value, + names=names, + module=module, + qualname=qualname, + type=type, + start=start, + ) + + try: + return super().__call__( + value, + names=names, # pyright: ignore[reportArgumentType] + module=module, + qualname=qualname, + type=type, + start=start, + ) + except ValueError: + return value diff --git a/zod-openapi/sdk/src/openapi/utils/eventstreaming.py b/zod-openapi/sdk/src/openapi/utils/eventstreaming.py new file mode 100644 index 0000000..0969899 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/utils/eventstreaming.py @@ -0,0 +1,248 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +import re +import json +from typing import ( + Callable, + Generic, + TypeVar, + Optional, + Generator, + AsyncGenerator, + Tuple, +) +import httpx + +T = TypeVar("T") + + +class EventStream(Generic[T]): + # Holds a reference to the SDK client to avoid it being garbage collected + # and cause termination of the underlying httpx client. + client_ref: Optional[object] + response: httpx.Response + generator: Generator[T, None, None] + + def __init__( + self, + response: httpx.Response, + decoder: Callable[[str], T], + sentinel: Optional[str] = None, + client_ref: Optional[object] = None, + ): + self.response = response + self.generator = stream_events(response, decoder, sentinel) + self.client_ref = client_ref + + def __iter__(self): + return self + + def __next__(self): + return next(self.generator) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.response.close() + + +class EventStreamAsync(Generic[T]): + # Holds a reference to the SDK client to avoid it being garbage collected + # and cause termination of the underlying httpx client. + client_ref: Optional[object] + response: httpx.Response + generator: AsyncGenerator[T, None] + + def __init__( + self, + response: httpx.Response, + decoder: Callable[[str], T], + sentinel: Optional[str] = None, + client_ref: Optional[object] = None, + ): + self.response = response + self.generator = stream_events_async(response, decoder, sentinel) + self.client_ref = client_ref + + def __aiter__(self): + return self + + async def __anext__(self): + return await self.generator.__anext__() + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.response.aclose() + + +class ServerEvent: + id: Optional[str] = None + event: Optional[str] = None + data: Optional[str] = None + retry: Optional[int] = None + + +MESSAGE_BOUNDARIES = [ + b"\r\n\r\n", + b"\n\n", + b"\r\r", +] + + +async def stream_events_async( + response: httpx.Response, + decoder: Callable[[str], T], + sentinel: Optional[str] = None, +) -> AsyncGenerator[T, None]: + buffer = bytearray() + position = 0 + discard = False + async for chunk in response.aiter_bytes(): + # We've encountered the sentinel value and should no longer process + # incoming data. Instead we throw new data away until the server closes + # the connection. + if discard: + continue + + buffer += chunk + for i in range(position, len(buffer)): + char = buffer[i : i + 1] + seq: Optional[bytes] = None + if char in [b"\r", b"\n"]: + for boundary in MESSAGE_BOUNDARIES: + seq = _peek_sequence(i, buffer, boundary) + if seq is not None: + break + if seq is None: + continue + + block = buffer[position:i] + position = i + len(seq) + event, discard = _parse_event(block, decoder, sentinel) + if event is not None: + yield event + + if position > 0: + buffer = buffer[position:] + position = 0 + + event, discard = _parse_event(buffer, decoder, sentinel) + if event is not None: + yield event + + +def stream_events( + response: httpx.Response, + decoder: Callable[[str], T], + sentinel: Optional[str] = None, +) -> Generator[T, None, None]: + buffer = bytearray() + position = 0 + discard = False + for chunk in response.iter_bytes(): + # We've encountered the sentinel value and should no longer process + # incoming data. Instead we throw new data away until the server closes + # the connection. + if discard: + continue + + buffer += chunk + for i in range(position, len(buffer)): + char = buffer[i : i + 1] + seq: Optional[bytes] = None + if char in [b"\r", b"\n"]: + for boundary in MESSAGE_BOUNDARIES: + seq = _peek_sequence(i, buffer, boundary) + if seq is not None: + break + if seq is None: + continue + + block = buffer[position:i] + position = i + len(seq) + event, discard = _parse_event(block, decoder, sentinel) + if event is not None: + yield event + + if position > 0: + buffer = buffer[position:] + position = 0 + + event, discard = _parse_event(buffer, decoder, sentinel) + if event is not None: + yield event + + +def _parse_event( + raw: bytearray, decoder: Callable[[str], T], sentinel: Optional[str] = None +) -> Tuple[Optional[T], bool]: + block = raw.decode() + lines = re.split(r"\r?\n|\r", block) + publish = False + event = ServerEvent() + data = "" + for line in lines: + if not line: + continue + + delim = line.find(":") + if delim <= 0: + continue + + field = line[0:delim] + value = line[delim + 1 :] if delim < len(line) - 1 else "" + if len(value) and value[0] == " ": + value = value[1:] + + if field == "event": + event.event = value + publish = True + elif field == "data": + data += value + "\n" + publish = True + elif field == "id": + event.id = value + publish = True + elif field == "retry": + event.retry = int(value) if value.isdigit() else None + publish = True + + if sentinel and data == f"{sentinel}\n": + return None, True + + if data: + data = data[:-1] + event.data = data + + data_is_primitive = ( + data.isnumeric() or data == "true" or data == "false" or data == "null" + ) + data_is_json = ( + data.startswith("{") or data.startswith("[") or data.startswith('"') + ) + + if data_is_primitive or data_is_json: + try: + event.data = json.loads(data) + except Exception: + pass + + out = None + if publish: + out = decoder(json.dumps(event.__dict__)) + + return out, False + + +def _peek_sequence(position: int, buffer: bytearray, sequence: bytes): + if len(sequence) > (len(buffer) - position): + return None + + for i, seq in enumerate(sequence): + if buffer[position + i] != seq: + return None + + return sequence diff --git a/zod-openapi/sdk/src/openapi/utils/forms.py b/zod-openapi/sdk/src/openapi/utils/forms.py new file mode 100644 index 0000000..e873495 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/utils/forms.py @@ -0,0 +1,223 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from typing import ( + Any, + Dict, + get_type_hints, + List, + Tuple, +) +from pydantic import BaseModel +from pydantic.fields import FieldInfo + +from .serializers import marshal_json + +from .metadata import ( + FormMetadata, + MultipartFormMetadata, + find_field_metadata, +) +from .values import _is_set, _val_to_string + + +def _populate_form( + field_name: str, + explode: bool, + obj: Any, + delimiter: str, + form: Dict[str, List[str]], +): + if not _is_set(obj): + return form + + if isinstance(obj, BaseModel): + items = [] + + obj_fields: Dict[str, FieldInfo] = obj.__class__.model_fields + for name in obj_fields: + obj_field = obj_fields[name] + obj_field_name = obj_field.alias if obj_field.alias is not None else name + if obj_field_name == "": + continue + + val = getattr(obj, name) + if not _is_set(val): + continue + + if explode: + form[obj_field_name] = [_val_to_string(val)] + else: + items.append(f"{obj_field_name}{delimiter}{_val_to_string(val)}") + + if len(items) > 0: + form[field_name] = [delimiter.join(items)] + elif isinstance(obj, Dict): + items = [] + for key, value in obj.items(): + if not _is_set(value): + continue + + if explode: + form[key] = [_val_to_string(value)] + else: + items.append(f"{key}{delimiter}{_val_to_string(value)}") + + if len(items) > 0: + form[field_name] = [delimiter.join(items)] + elif isinstance(obj, List): + items = [] + + for value in obj: + if not _is_set(value): + continue + + if explode: + if not field_name in form: + form[field_name] = [] + form[field_name].append(_val_to_string(value)) + else: + items.append(_val_to_string(value)) + + if len(items) > 0: + form[field_name] = [delimiter.join([str(item) for item in items])] + else: + form[field_name] = [_val_to_string(obj)] + + return form + + +def _extract_file_properties(file_obj: Any) -> Tuple[str, Any, Any]: + """Extract file name, content, and content type from a file object.""" + file_fields: Dict[str, FieldInfo] = file_obj.__class__.model_fields + + file_name = "" + content = None + content_type = None + + for file_field_name in file_fields: + file_field = file_fields[file_field_name] + + file_metadata = find_field_metadata(file_field, MultipartFormMetadata) + if file_metadata is None: + continue + + if file_metadata.content: + content = getattr(file_obj, file_field_name, None) + elif file_field_name == "content_type": + content_type = getattr(file_obj, file_field_name, None) + else: + file_name = getattr(file_obj, file_field_name) + + if file_name == "" or content is None: + raise ValueError("invalid multipart/form-data file") + + return file_name, content, content_type + + +def serialize_multipart_form( + media_type: str, request: Any +) -> Tuple[str, Dict[str, Any], List[Tuple[str, Any]]]: + form: Dict[str, Any] = {} + files: List[Tuple[str, Any]] = [] + + if not isinstance(request, BaseModel): + raise TypeError("invalid request body type") + + request_fields: Dict[str, FieldInfo] = request.__class__.model_fields + request_field_types = get_type_hints(request.__class__) + + for name in request_fields: + field = request_fields[name] + + val = getattr(request, name) + if not _is_set(val): + continue + + field_metadata = find_field_metadata(field, MultipartFormMetadata) + if not field_metadata: + continue + + f_name = field.alias if field.alias else name + + if field_metadata.file: + if isinstance(val, List): + # Handle array of files + for file_obj in val: + if not _is_set(file_obj): + continue + + file_name, content, content_type = _extract_file_properties(file_obj) + + if content_type is not None: + files.append((f_name + "[]", (file_name, content, content_type))) + else: + files.append((f_name + "[]", (file_name, content))) + else: + # Handle single file + file_name, content, content_type = _extract_file_properties(val) + + if content_type is not None: + files.append((f_name, (file_name, content, content_type))) + else: + files.append((f_name, (file_name, content))) + elif field_metadata.json: + files.append((f_name, ( + None, + marshal_json(val, request_field_types[name]), + "application/json", + ))) + else: + if isinstance(val, List): + values = [] + + for value in val: + if not _is_set(value): + continue + values.append(_val_to_string(value)) + + form[f_name + "[]"] = values + else: + form[f_name] = _val_to_string(val) + return media_type, form, files + + +def serialize_form_data(data: Any) -> Dict[str, Any]: + form: Dict[str, List[str]] = {} + + if isinstance(data, BaseModel): + data_fields: Dict[str, FieldInfo] = data.__class__.model_fields + data_field_types = get_type_hints(data.__class__) + for name in data_fields: + field = data_fields[name] + + val = getattr(data, name) + if not _is_set(val): + continue + + metadata = find_field_metadata(field, FormMetadata) + if metadata is None: + continue + + f_name = field.alias if field.alias is not None else name + + if metadata.json: + form[f_name] = [marshal_json(val, data_field_types[name])] + else: + if metadata.style == "form": + _populate_form( + f_name, + metadata.explode, + val, + ",", + form, + ) + else: + raise ValueError(f"Invalid form style for field {name}") + elif isinstance(data, Dict): + for key, value in data.items(): + if _is_set(value): + form[key] = [_val_to_string(value)] + else: + raise TypeError(f"Invalid request body type {type(data)} for form data") + + return form diff --git a/zod-openapi/sdk/src/openapi/utils/headers.py b/zod-openapi/sdk/src/openapi/utils/headers.py new file mode 100644 index 0000000..37864cb --- /dev/null +++ b/zod-openapi/sdk/src/openapi/utils/headers.py @@ -0,0 +1,136 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from typing import ( + Any, + Dict, + List, + Optional, +) +from httpx import Headers +from pydantic import BaseModel +from pydantic.fields import FieldInfo + +from .metadata import ( + HeaderMetadata, + find_field_metadata, +) + +from .values import _is_set, _populate_from_globals, _val_to_string + + +def get_headers(headers_params: Any, gbls: Optional[Any] = None) -> Dict[str, str]: + headers: Dict[str, str] = {} + + globals_already_populated = [] + if _is_set(headers_params): + globals_already_populated = _populate_headers(headers_params, gbls, headers, []) + if _is_set(gbls): + _populate_headers(gbls, None, headers, globals_already_populated) + + return headers + + +def _populate_headers( + headers_params: Any, + gbls: Any, + header_values: Dict[str, str], + skip_fields: List[str], +) -> List[str]: + globals_already_populated: List[str] = [] + + if not isinstance(headers_params, BaseModel): + return globals_already_populated + + param_fields: Dict[str, FieldInfo] = headers_params.__class__.model_fields + for name in param_fields: + if name in skip_fields: + continue + + field = param_fields[name] + f_name = field.alias if field.alias is not None else name + + metadata = find_field_metadata(field, HeaderMetadata) + if metadata is None: + continue + + value, global_found = _populate_from_globals( + name, getattr(headers_params, name), HeaderMetadata, gbls + ) + if global_found: + globals_already_populated.append(name) + value = _serialize_header(metadata.explode, value) + + if value != "": + header_values[f_name] = value + + return globals_already_populated + + +def _serialize_header(explode: bool, obj: Any) -> str: + if not _is_set(obj): + return "" + + if isinstance(obj, BaseModel): + items = [] + obj_fields: Dict[str, FieldInfo] = obj.__class__.model_fields + for name in obj_fields: + obj_field = obj_fields[name] + obj_param_metadata = find_field_metadata(obj_field, HeaderMetadata) + + if not obj_param_metadata: + continue + + f_name = obj_field.alias if obj_field.alias is not None else name + + val = getattr(obj, name) + if not _is_set(val): + continue + + if explode: + items.append(f"{f_name}={_val_to_string(val)}") + else: + items.append(f_name) + items.append(_val_to_string(val)) + + if len(items) > 0: + return ",".join(items) + elif isinstance(obj, Dict): + items = [] + + for key, value in obj.items(): + if not _is_set(value): + continue + + if explode: + items.append(f"{key}={_val_to_string(value)}") + else: + items.append(key) + items.append(_val_to_string(value)) + + if len(items) > 0: + return ",".join([str(item) for item in items]) + elif isinstance(obj, List): + items = [] + + for value in obj: + if not _is_set(value): + continue + + items.append(_val_to_string(value)) + + if len(items) > 0: + return ",".join(items) + elif _is_set(obj): + return f"{_val_to_string(obj)}" + + return "" + + +def get_response_headers(headers: Headers) -> Dict[str, List[str]]: + res: Dict[str, List[str]] = {} + for k, v in headers.items(): + if not k in res: + res[k] = [] + + res[k].append(v) + return res diff --git a/zod-openapi/sdk/src/openapi/utils/logger.py b/zod-openapi/sdk/src/openapi/utils/logger.py new file mode 100644 index 0000000..b661aff --- /dev/null +++ b/zod-openapi/sdk/src/openapi/utils/logger.py @@ -0,0 +1,22 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +import httpx +from typing import Any, Protocol + + +class Logger(Protocol): + def debug(self, msg: str, *args: Any, **kwargs: Any) -> None: + pass + + +class NoOpLogger: + def debug(self, msg: str, *args: Any, **kwargs: Any) -> None: + pass + + +def get_body_content(req: httpx.Request) -> str: + return "" if not hasattr(req, "_content") else str(req.content) + + +def get_default_logger() -> Logger: + return NoOpLogger() diff --git a/zod-openapi/sdk/src/openapi/utils/metadata.py b/zod-openapi/sdk/src/openapi/utils/metadata.py new file mode 100644 index 0000000..173b3e5 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/utils/metadata.py @@ -0,0 +1,118 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from typing import Optional, Type, TypeVar, Union +from dataclasses import dataclass +from pydantic.fields import FieldInfo + + +T = TypeVar("T") + + +@dataclass +class SecurityMetadata: + option: bool = False + scheme: bool = False + scheme_type: Optional[str] = None + sub_type: Optional[str] = None + field_name: Optional[str] = None + + def get_field_name(self, default: str) -> str: + return self.field_name or default + + +@dataclass +class ParamMetadata: + serialization: Optional[str] = None + style: str = "simple" + explode: bool = False + + +@dataclass +class PathParamMetadata(ParamMetadata): + pass + + +@dataclass +class QueryParamMetadata(ParamMetadata): + style: str = "form" + explode: bool = True + + +@dataclass +class HeaderMetadata(ParamMetadata): + pass + + +@dataclass +class RequestMetadata: + media_type: str = "application/octet-stream" + + +@dataclass +class MultipartFormMetadata: + file: bool = False + content: bool = False + json: bool = False + + +@dataclass +class FormMetadata: + json: bool = False + style: str = "form" + explode: bool = True + + +class FieldMetadata: + security: Optional[SecurityMetadata] = None + path: Optional[PathParamMetadata] = None + query: Optional[QueryParamMetadata] = None + header: Optional[HeaderMetadata] = None + request: Optional[RequestMetadata] = None + form: Optional[FormMetadata] = None + multipart: Optional[MultipartFormMetadata] = None + + def __init__( + self, + security: Optional[SecurityMetadata] = None, + path: Optional[Union[PathParamMetadata, bool]] = None, + query: Optional[Union[QueryParamMetadata, bool]] = None, + header: Optional[Union[HeaderMetadata, bool]] = None, + request: Optional[Union[RequestMetadata, bool]] = None, + form: Optional[Union[FormMetadata, bool]] = None, + multipart: Optional[Union[MultipartFormMetadata, bool]] = None, + ): + self.security = security + self.path = PathParamMetadata() if isinstance(path, bool) else path + self.query = QueryParamMetadata() if isinstance(query, bool) else query + self.header = HeaderMetadata() if isinstance(header, bool) else header + self.request = RequestMetadata() if isinstance(request, bool) else request + self.form = FormMetadata() if isinstance(form, bool) else form + self.multipart = ( + MultipartFormMetadata() if isinstance(multipart, bool) else multipart + ) + + +def find_field_metadata(field_info: FieldInfo, metadata_type: Type[T]) -> Optional[T]: + metadata = find_metadata(field_info, FieldMetadata) + if not metadata: + return None + + fields = metadata.__dict__ + + for field in fields: + if isinstance(fields[field], metadata_type): + return fields[field] + + return None + + +def find_metadata(field_info: FieldInfo, metadata_type: Type[T]) -> Optional[T]: + metadata = field_info.metadata + if not metadata: + return None + + for md in metadata: + if isinstance(md, metadata_type): + return md + + return None diff --git a/zod-openapi/sdk/src/openapi/utils/queryparams.py b/zod-openapi/sdk/src/openapi/utils/queryparams.py new file mode 100644 index 0000000..37a6e7f --- /dev/null +++ b/zod-openapi/sdk/src/openapi/utils/queryparams.py @@ -0,0 +1,205 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from typing import ( + Any, + Dict, + get_type_hints, + List, + Optional, +) + +from pydantic import BaseModel +from pydantic.fields import FieldInfo + +from .metadata import ( + QueryParamMetadata, + find_field_metadata, +) +from .values import ( + _get_serialized_params, + _is_set, + _populate_from_globals, + _val_to_string, +) +from .forms import _populate_form + + +def get_query_params( + query_params: Any, + gbls: Optional[Any] = None, +) -> Dict[str, List[str]]: + params: Dict[str, List[str]] = {} + + globals_already_populated = _populate_query_params(query_params, gbls, params, []) + if _is_set(gbls): + _populate_query_params(gbls, None, params, globals_already_populated) + + return params + + +def _populate_query_params( + query_params: Any, + gbls: Any, + query_param_values: Dict[str, List[str]], + skip_fields: List[str], +) -> List[str]: + globals_already_populated: List[str] = [] + + if not isinstance(query_params, BaseModel): + return globals_already_populated + + param_fields: Dict[str, FieldInfo] = query_params.__class__.model_fields + param_field_types = get_type_hints(query_params.__class__) + for name in param_fields: + if name in skip_fields: + continue + + field = param_fields[name] + + metadata = find_field_metadata(field, QueryParamMetadata) + if not metadata: + continue + + value = getattr(query_params, name) if _is_set(query_params) else None + + value, global_found = _populate_from_globals( + name, value, QueryParamMetadata, gbls + ) + if global_found: + globals_already_populated.append(name) + + f_name = field.alias if field.alias is not None else name + serialization = metadata.serialization + if serialization is not None: + serialized_parms = _get_serialized_params( + metadata, f_name, value, param_field_types[name] + ) + for key, value in serialized_parms.items(): + if key in query_param_values: + query_param_values[key].extend(value) + else: + query_param_values[key] = [value] + else: + style = metadata.style + if style == "deepObject": + _populate_deep_object_query_params(f_name, value, query_param_values) + elif style == "form": + _populate_delimited_query_params( + metadata, f_name, value, ",", query_param_values + ) + elif style == "pipeDelimited": + _populate_delimited_query_params( + metadata, f_name, value, "|", query_param_values + ) + else: + raise NotImplementedError( + f"query param style {style} not yet supported" + ) + + return globals_already_populated + + +def _populate_deep_object_query_params( + field_name: str, + obj: Any, + params: Dict[str, List[str]], +): + if not _is_set(obj): + return + + if isinstance(obj, BaseModel): + _populate_deep_object_query_params_basemodel(field_name, obj, params) + elif isinstance(obj, Dict): + _populate_deep_object_query_params_dict(field_name, obj, params) + + +def _populate_deep_object_query_params_basemodel( + prior_params_key: str, + obj: Any, + params: Dict[str, List[str]], +): + if not _is_set(obj) or not isinstance(obj, BaseModel): + return + + obj_fields: Dict[str, FieldInfo] = obj.__class__.model_fields + for name in obj_fields: + obj_field = obj_fields[name] + + f_name = obj_field.alias if obj_field.alias is not None else name + + params_key = f"{prior_params_key}[{f_name}]" + + obj_param_metadata = find_field_metadata(obj_field, QueryParamMetadata) + if not _is_set(obj_param_metadata): + continue + + obj_val = getattr(obj, name) + if not _is_set(obj_val): + continue + + if isinstance(obj_val, BaseModel): + _populate_deep_object_query_params_basemodel(params_key, obj_val, params) + elif isinstance(obj_val, Dict): + _populate_deep_object_query_params_dict(params_key, obj_val, params) + elif isinstance(obj_val, List): + _populate_deep_object_query_params_list(params_key, obj_val, params) + else: + params[params_key] = [_val_to_string(obj_val)] + + +def _populate_deep_object_query_params_dict( + prior_params_key: str, + value: Dict, + params: Dict[str, List[str]], +): + if not _is_set(value): + return + + for key, val in value.items(): + if not _is_set(val): + continue + + params_key = f"{prior_params_key}[{key}]" + + if isinstance(val, BaseModel): + _populate_deep_object_query_params_basemodel(params_key, val, params) + elif isinstance(val, Dict): + _populate_deep_object_query_params_dict(params_key, val, params) + elif isinstance(val, List): + _populate_deep_object_query_params_list(params_key, val, params) + else: + params[params_key] = [_val_to_string(val)] + + +def _populate_deep_object_query_params_list( + params_key: str, + value: List, + params: Dict[str, List[str]], +): + if not _is_set(value): + return + + for val in value: + if not _is_set(val): + continue + + if params.get(params_key) is None: + params[params_key] = [] + + params[params_key].append(_val_to_string(val)) + + +def _populate_delimited_query_params( + metadata: QueryParamMetadata, + field_name: str, + obj: Any, + delimiter: str, + query_param_values: Dict[str, List[str]], +): + _populate_form( + field_name, + metadata.explode, + obj, + delimiter, + query_param_values, + ) diff --git a/zod-openapi/sdk/src/openapi/utils/requestbodies.py b/zod-openapi/sdk/src/openapi/utils/requestbodies.py new file mode 100644 index 0000000..d5240dd --- /dev/null +++ b/zod-openapi/sdk/src/openapi/utils/requestbodies.py @@ -0,0 +1,66 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +import io +from dataclasses import dataclass +import re +from typing import ( + Any, + Optional, +) + +from .forms import serialize_form_data, serialize_multipart_form + +from .serializers import marshal_json + +SERIALIZATION_METHOD_TO_CONTENT_TYPE = { + "json": "application/json", + "form": "application/x-www-form-urlencoded", + "multipart": "multipart/form-data", + "raw": "application/octet-stream", + "string": "text/plain", +} + + +@dataclass +class SerializedRequestBody: + media_type: Optional[str] = None + content: Optional[Any] = None + data: Optional[Any] = None + files: Optional[Any] = None + + +def serialize_request_body( + request_body: Any, + nullable: bool, + optional: bool, + serialization_method: str, + request_body_type, +) -> Optional[SerializedRequestBody]: + if request_body is None: + if not nullable and optional: + return None + + media_type = SERIALIZATION_METHOD_TO_CONTENT_TYPE[serialization_method] + + serialized_request_body = SerializedRequestBody(media_type) + + if re.match(r"(application|text)\/.*?\+*json.*", media_type) is not None: + serialized_request_body.content = marshal_json(request_body, request_body_type) + elif re.match(r"multipart\/.*", media_type) is not None: + ( + serialized_request_body.media_type, + serialized_request_body.data, + serialized_request_body.files, + ) = serialize_multipart_form(media_type, request_body) + elif re.match(r"application\/x-www-form-urlencoded.*", media_type) is not None: + serialized_request_body.data = serialize_form_data(request_body) + elif isinstance(request_body, (bytes, bytearray, io.BytesIO, io.BufferedReader)): + serialized_request_body.content = request_body + elif isinstance(request_body, str): + serialized_request_body.content = request_body + else: + raise TypeError( + f"invalid request body type {type(request_body)} for mediaType {media_type}" + ) + + return serialized_request_body diff --git a/zod-openapi/sdk/src/openapi/utils/retries.py b/zod-openapi/sdk/src/openapi/utils/retries.py new file mode 100644 index 0000000..4d60867 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/utils/retries.py @@ -0,0 +1,217 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +import asyncio +import random +import time +from typing import List + +import httpx + + +class BackoffStrategy: + initial_interval: int + max_interval: int + exponent: float + max_elapsed_time: int + + def __init__( + self, + initial_interval: int, + max_interval: int, + exponent: float, + max_elapsed_time: int, + ): + self.initial_interval = initial_interval + self.max_interval = max_interval + self.exponent = exponent + self.max_elapsed_time = max_elapsed_time + + +class RetryConfig: + strategy: str + backoff: BackoffStrategy + retry_connection_errors: bool + + def __init__( + self, strategy: str, backoff: BackoffStrategy, retry_connection_errors: bool + ): + self.strategy = strategy + self.backoff = backoff + self.retry_connection_errors = retry_connection_errors + + +class Retries: + config: RetryConfig + status_codes: List[str] + + def __init__(self, config: RetryConfig, status_codes: List[str]): + self.config = config + self.status_codes = status_codes + + +class TemporaryError(Exception): + response: httpx.Response + + def __init__(self, response: httpx.Response): + self.response = response + + +class PermanentError(Exception): + inner: Exception + + def __init__(self, inner: Exception): + self.inner = inner + + +def retry(func, retries: Retries): + if retries.config.strategy == "backoff": + + def do_request() -> httpx.Response: + res: httpx.Response + try: + res = func() + + for code in retries.status_codes: + if "X" in code.upper(): + code_range = int(code[0]) + + status_major = res.status_code / 100 + + if code_range <= status_major < code_range + 1: + raise TemporaryError(res) + else: + parsed_code = int(code) + + if res.status_code == parsed_code: + raise TemporaryError(res) + except httpx.ConnectError as exception: + if retries.config.retry_connection_errors: + raise + + raise PermanentError(exception) from exception + except httpx.TimeoutException as exception: + if retries.config.retry_connection_errors: + raise + + raise PermanentError(exception) from exception + except TemporaryError: + raise + except Exception as exception: + raise PermanentError(exception) from exception + + return res + + return retry_with_backoff( + do_request, + retries.config.backoff.initial_interval, + retries.config.backoff.max_interval, + retries.config.backoff.exponent, + retries.config.backoff.max_elapsed_time, + ) + + return func() + + +async def retry_async(func, retries: Retries): + if retries.config.strategy == "backoff": + + async def do_request() -> httpx.Response: + res: httpx.Response + try: + res = await func() + + for code in retries.status_codes: + if "X" in code.upper(): + code_range = int(code[0]) + + status_major = res.status_code / 100 + + if code_range <= status_major < code_range + 1: + raise TemporaryError(res) + else: + parsed_code = int(code) + + if res.status_code == parsed_code: + raise TemporaryError(res) + except httpx.ConnectError as exception: + if retries.config.retry_connection_errors: + raise + + raise PermanentError(exception) from exception + except httpx.TimeoutException as exception: + if retries.config.retry_connection_errors: + raise + + raise PermanentError(exception) from exception + except TemporaryError: + raise + except Exception as exception: + raise PermanentError(exception) from exception + + return res + + return await retry_with_backoff_async( + do_request, + retries.config.backoff.initial_interval, + retries.config.backoff.max_interval, + retries.config.backoff.exponent, + retries.config.backoff.max_elapsed_time, + ) + + return await func() + + +def retry_with_backoff( + func, + initial_interval=500, + max_interval=60000, + exponent=1.5, + max_elapsed_time=3600000, +): + start = round(time.time() * 1000) + retries = 0 + + while True: + try: + return func() + except PermanentError as exception: + raise exception.inner + except Exception as exception: # pylint: disable=broad-exception-caught + now = round(time.time() * 1000) + if now - start > max_elapsed_time: + if isinstance(exception, TemporaryError): + return exception.response + + raise + sleep = (initial_interval / 1000) * exponent**retries + random.uniform(0, 1) + sleep = min(sleep, max_interval / 1000) + time.sleep(sleep) + retries += 1 + + +async def retry_with_backoff_async( + func, + initial_interval=500, + max_interval=60000, + exponent=1.5, + max_elapsed_time=3600000, +): + start = round(time.time() * 1000) + retries = 0 + + while True: + try: + return await func() + except PermanentError as exception: + raise exception.inner + except Exception as exception: # pylint: disable=broad-exception-caught + now = round(time.time() * 1000) + if now - start > max_elapsed_time: + if isinstance(exception, TemporaryError): + return exception.response + + raise + sleep = (initial_interval / 1000) * exponent**retries + random.uniform(0, 1) + sleep = min(sleep, max_interval / 1000) + await asyncio.sleep(sleep) + retries += 1 diff --git a/zod-openapi/sdk/src/openapi/utils/security.py b/zod-openapi/sdk/src/openapi/utils/security.py new file mode 100644 index 0000000..295a3f4 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/utils/security.py @@ -0,0 +1,174 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +import base64 +from typing import ( + Any, + Dict, + List, + Tuple, +) +from pydantic import BaseModel +from pydantic.fields import FieldInfo + +from .metadata import ( + SecurityMetadata, + find_field_metadata, +) + + +def get_security(security: Any) -> Tuple[Dict[str, str], Dict[str, List[str]]]: + headers: Dict[str, str] = {} + query_params: Dict[str, List[str]] = {} + + if security is None: + return headers, query_params + + if not isinstance(security, BaseModel): + raise TypeError("security must be a pydantic model") + + sec_fields: Dict[str, FieldInfo] = security.__class__.model_fields + for name in sec_fields: + sec_field = sec_fields[name] + + value = getattr(security, name) + if value is None: + continue + + metadata = find_field_metadata(sec_field, SecurityMetadata) + if metadata is None: + continue + if metadata.option: + _parse_security_option(headers, query_params, value) + return headers, query_params + if metadata.scheme: + # Special case for basic auth or custom auth which could be a flattened model + if metadata.sub_type in ["basic", "custom"] and not isinstance( + value, BaseModel + ): + _parse_security_scheme(headers, query_params, metadata, name, security) + else: + _parse_security_scheme(headers, query_params, metadata, name, value) + + return headers, query_params + + +def _parse_security_option( + headers: Dict[str, str], query_params: Dict[str, List[str]], option: Any +): + if not isinstance(option, BaseModel): + raise TypeError("security option must be a pydantic model") + + opt_fields: Dict[str, FieldInfo] = option.__class__.model_fields + for name in opt_fields: + opt_field = opt_fields[name] + + metadata = find_field_metadata(opt_field, SecurityMetadata) + if metadata is None or not metadata.scheme: + continue + _parse_security_scheme( + headers, query_params, metadata, name, getattr(option, name) + ) + + +def _parse_security_scheme( + headers: Dict[str, str], + query_params: Dict[str, List[str]], + scheme_metadata: SecurityMetadata, + field_name: str, + scheme: Any, +): + scheme_type = scheme_metadata.scheme_type + sub_type = scheme_metadata.sub_type + + if isinstance(scheme, BaseModel): + if scheme_type == "http": + if sub_type == "basic": + _parse_basic_auth_scheme(headers, scheme) + return + if sub_type == "custom": + return + + scheme_fields: Dict[str, FieldInfo] = scheme.__class__.model_fields + for name in scheme_fields: + scheme_field = scheme_fields[name] + + metadata = find_field_metadata(scheme_field, SecurityMetadata) + if metadata is None or metadata.field_name is None: + continue + + value = getattr(scheme, name) + + _parse_security_scheme_value( + headers, query_params, scheme_metadata, metadata, name, value + ) + else: + _parse_security_scheme_value( + headers, query_params, scheme_metadata, scheme_metadata, field_name, scheme + ) + + +def _parse_security_scheme_value( + headers: Dict[str, str], + query_params: Dict[str, List[str]], + scheme_metadata: SecurityMetadata, + security_metadata: SecurityMetadata, + field_name: str, + value: Any, +): + scheme_type = scheme_metadata.scheme_type + sub_type = scheme_metadata.sub_type + + header_name = security_metadata.get_field_name(field_name) + + if scheme_type == "apiKey": + if sub_type == "header": + headers[header_name] = value + elif sub_type == "query": + query_params[header_name] = [value] + else: + raise ValueError("sub type {sub_type} not supported") + elif scheme_type == "openIdConnect": + headers[header_name] = _apply_bearer(value) + elif scheme_type == "oauth2": + if sub_type != "client_credentials": + headers[header_name] = _apply_bearer(value) + elif scheme_type == "http": + if sub_type == "bearer": + headers[header_name] = _apply_bearer(value) + elif sub_type == "custom": + return + else: + raise ValueError("sub type {sub_type} not supported") + else: + raise ValueError("scheme type {scheme_type} not supported") + + +def _apply_bearer(token: str) -> str: + return token.lower().startswith("bearer ") and token or f"Bearer {token}" + + +def _parse_basic_auth_scheme(headers: Dict[str, str], scheme: Any): + username = "" + password = "" + + if not isinstance(scheme, BaseModel): + raise TypeError("basic auth scheme must be a pydantic model") + + scheme_fields: Dict[str, FieldInfo] = scheme.__class__.model_fields + for name in scheme_fields: + scheme_field = scheme_fields[name] + + metadata = find_field_metadata(scheme_field, SecurityMetadata) + if metadata is None or metadata.field_name is None: + continue + + field_name = metadata.field_name + value = getattr(scheme, name) + + if field_name == "username": + username = value + if field_name == "password": + password = value + + data = f"{username}:{password}".encode() + headers["Authorization"] = f"Basic {base64.b64encode(data).decode()}" diff --git a/zod-openapi/sdk/src/openapi/utils/serializers.py b/zod-openapi/sdk/src/openapi/utils/serializers.py new file mode 100644 index 0000000..378a14c --- /dev/null +++ b/zod-openapi/sdk/src/openapi/utils/serializers.py @@ -0,0 +1,249 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from decimal import Decimal +import functools +import json +import typing +from typing import Any, Dict, List, Tuple, Union, get_args +import typing_extensions +from typing_extensions import get_origin + +import httpx +from pydantic import ConfigDict, create_model +from pydantic_core import from_json + +from ..types.basemodel import BaseModel, Nullable, OptionalNullable, Unset + + +def serialize_decimal(as_str: bool): + def serialize(d): + # Optional[T] is a Union[T, None] + if is_union(type(d)) and type(None) in get_args(type(d)) and d is None: + return None + if isinstance(d, Unset): + return d + + if not isinstance(d, Decimal): + raise ValueError("Expected Decimal object") + + return str(d) if as_str else float(d) + + return serialize + + +def validate_decimal(d): + if d is None: + return None + + if isinstance(d, (Decimal, Unset)): + return d + + if not isinstance(d, (str, int, float)): + raise ValueError("Expected string, int or float") + + return Decimal(str(d)) + + +def serialize_float(as_str: bool): + def serialize(f): + # Optional[T] is a Union[T, None] + if is_union(type(f)) and type(None) in get_args(type(f)) and f is None: + return None + if isinstance(f, Unset): + return f + + if not isinstance(f, float): + raise ValueError("Expected float") + + return str(f) if as_str else f + + return serialize + + +def validate_float(f): + if f is None: + return None + + if isinstance(f, (float, Unset)): + return f + + if not isinstance(f, str): + raise ValueError("Expected string") + + return float(f) + + +def serialize_int(as_str: bool): + def serialize(i): + # Optional[T] is a Union[T, None] + if is_union(type(i)) and type(None) in get_args(type(i)) and i is None: + return None + if isinstance(i, Unset): + return i + + if not isinstance(i, int): + raise ValueError("Expected int") + + return str(i) if as_str else i + + return serialize + + +def validate_int(b): + if b is None: + return None + + if isinstance(b, (int, Unset)): + return b + + if not isinstance(b, str): + raise ValueError("Expected string") + + return int(b) + + +def validate_open_enum(is_int: bool): + def validate(e): + if e is None: + return None + + if isinstance(e, Unset): + return e + + if is_int: + if not isinstance(e, int): + raise ValueError("Expected int") + else: + if not isinstance(e, str): + raise ValueError("Expected string") + + return e + + return validate + + +def validate_const(v): + def validate(c): + # Optional[T] is a Union[T, None] + if is_union(type(c)) and type(None) in get_args(type(c)) and c is None: + return None + + if v != c: + raise ValueError(f"Expected {v}") + + return c + + return validate + + +def unmarshal_json(raw, typ: Any) -> Any: + return unmarshal(from_json(raw), typ) + + +def unmarshal(val, typ: Any) -> Any: + unmarshaller = create_model( + "Unmarshaller", + body=(typ, ...), + __config__=ConfigDict(populate_by_name=True, arbitrary_types_allowed=True), + ) + + m = unmarshaller(body=val) + + # pyright: ignore[reportAttributeAccessIssue] + return m.body # type: ignore + + +def marshal_json(val, typ): + if is_nullable(typ) and val is None: + return "null" + + marshaller = create_model( + "Marshaller", + body=(typ, ...), + __config__=ConfigDict(populate_by_name=True, arbitrary_types_allowed=True), + ) + + m = marshaller(body=val) + + d = m.model_dump(by_alias=True, mode="json", exclude_none=True) + + if len(d) == 0: + return "" + + return json.dumps(d[next(iter(d))], separators=(",", ":")) + + +def is_nullable(field): + origin = get_origin(field) + if origin is Nullable or origin is OptionalNullable: + return True + + if not origin is Union or type(None) not in get_args(field): + return False + + for arg in get_args(field): + if get_origin(arg) is Nullable or get_origin(arg) is OptionalNullable: + return True + + return False + + +def is_union(obj: object) -> bool: + """ + Returns True if the given object is a typing.Union or typing_extensions.Union. + """ + return any( + obj is typing_obj for typing_obj in _get_typing_objects_by_name_of("Union") + ) + + +def stream_to_text(stream: httpx.Response) -> str: + return "".join(stream.iter_text()) + + +async def stream_to_text_async(stream: httpx.Response) -> str: + return "".join([chunk async for chunk in stream.aiter_text()]) + + +def stream_to_bytes(stream: httpx.Response) -> bytes: + return stream.content + + +async def stream_to_bytes_async(stream: httpx.Response) -> bytes: + return await stream.aread() + + +def get_pydantic_model(data: Any, typ: Any) -> Any: + if not _contains_pydantic_model(data): + return unmarshal(data, typ) + + return data + + +def _contains_pydantic_model(data: Any) -> bool: + if isinstance(data, BaseModel): + return True + if isinstance(data, List): + return any(_contains_pydantic_model(item) for item in data) + if isinstance(data, Dict): + return any(_contains_pydantic_model(value) for value in data.values()) + + return False + + +@functools.cache +def _get_typing_objects_by_name_of(name: str) -> Tuple[Any, ...]: + """ + Get typing objects by name from typing and typing_extensions. + Reference: https://typing-extensions.readthedocs.io/en/latest/#runtime-use-of-types + """ + result = tuple( + getattr(module, name) + for module in (typing, typing_extensions) + if hasattr(module, name) + ) + if not result: + raise ValueError( + f"Neither typing nor typing_extensions has an object called {name!r}" + ) + return result diff --git a/zod-openapi/sdk/src/openapi/utils/unmarshal_json_response.py b/zod-openapi/sdk/src/openapi/utils/unmarshal_json_response.py new file mode 100644 index 0000000..654785e --- /dev/null +++ b/zod-openapi/sdk/src/openapi/utils/unmarshal_json_response.py @@ -0,0 +1,24 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from typing import Any, Optional + +import httpx + +from .serializers import unmarshal_json +from openapi import errors + + +def unmarshal_json_response( + typ: Any, http_res: httpx.Response, body: Optional[str] = None +) -> Any: + if body is None: + body = http_res.text + try: + return unmarshal_json(body, typ) + except Exception as e: + raise errors.ResponseValidationError( + "Response validation failed", + http_res, + e, + body, + ) from e diff --git a/zod-openapi/sdk/src/openapi/utils/url.py b/zod-openapi/sdk/src/openapi/utils/url.py new file mode 100644 index 0000000..c78ccba --- /dev/null +++ b/zod-openapi/sdk/src/openapi/utils/url.py @@ -0,0 +1,155 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from decimal import Decimal +from typing import ( + Any, + Dict, + get_type_hints, + List, + Optional, + Union, + get_args, + get_origin, +) +from pydantic import BaseModel +from pydantic.fields import FieldInfo + +from .metadata import ( + PathParamMetadata, + find_field_metadata, +) +from .values import ( + _get_serialized_params, + _is_set, + _populate_from_globals, + _val_to_string, +) + + +def generate_url( + server_url: str, + path: str, + path_params: Any, + gbls: Optional[Any] = None, +) -> str: + path_param_values: Dict[str, str] = {} + + globals_already_populated = _populate_path_params( + path_params, gbls, path_param_values, [] + ) + if _is_set(gbls): + _populate_path_params(gbls, None, path_param_values, globals_already_populated) + + for key, value in path_param_values.items(): + path = path.replace("{" + key + "}", value, 1) + + return remove_suffix(server_url, "/") + path + + +def _populate_path_params( + path_params: Any, + gbls: Any, + path_param_values: Dict[str, str], + skip_fields: List[str], +) -> List[str]: + globals_already_populated: List[str] = [] + + if not isinstance(path_params, BaseModel): + return globals_already_populated + + path_param_fields: Dict[str, FieldInfo] = path_params.__class__.model_fields + path_param_field_types = get_type_hints(path_params.__class__) + for name in path_param_fields: + if name in skip_fields: + continue + + field = path_param_fields[name] + + param_metadata = find_field_metadata(field, PathParamMetadata) + if param_metadata is None: + continue + + param = getattr(path_params, name) if _is_set(path_params) else None + param, global_found = _populate_from_globals( + name, param, PathParamMetadata, gbls + ) + if global_found: + globals_already_populated.append(name) + + if not _is_set(param): + continue + + f_name = field.alias if field.alias is not None else name + serialization = param_metadata.serialization + if serialization is not None: + serialized_params = _get_serialized_params( + param_metadata, f_name, param, path_param_field_types[name] + ) + for key, value in serialized_params.items(): + path_param_values[key] = value + else: + pp_vals: List[str] = [] + if param_metadata.style == "simple": + if isinstance(param, List): + for pp_val in param: + if not _is_set(pp_val): + continue + pp_vals.append(_val_to_string(pp_val)) + path_param_values[f_name] = ",".join(pp_vals) + elif isinstance(param, Dict): + for pp_key in param: + if not _is_set(param[pp_key]): + continue + if param_metadata.explode: + pp_vals.append(f"{pp_key}={_val_to_string(param[pp_key])}") + else: + pp_vals.append(f"{pp_key},{_val_to_string(param[pp_key])}") + path_param_values[f_name] = ",".join(pp_vals) + elif not isinstance(param, (str, int, float, complex, bool, Decimal)): + param_fields: Dict[str, FieldInfo] = param.__class__.model_fields + for name in param_fields: + param_field = param_fields[name] + + param_value_metadata = find_field_metadata( + param_field, PathParamMetadata + ) + if param_value_metadata is None: + continue + + param_name = ( + param_field.alias if param_field.alias is not None else name + ) + + param_field_val = getattr(param, name) + if not _is_set(param_field_val): + continue + if param_metadata.explode: + pp_vals.append( + f"{param_name}={_val_to_string(param_field_val)}" + ) + else: + pp_vals.append( + f"{param_name},{_val_to_string(param_field_val)}" + ) + path_param_values[f_name] = ",".join(pp_vals) + elif _is_set(param): + path_param_values[f_name] = _val_to_string(param) + + return globals_already_populated + + +def is_optional(field): + return get_origin(field) is Union and type(None) in get_args(field) + + +def template_url(url_with_params: str, params: Dict[str, str]) -> str: + for key, value in params.items(): + url_with_params = url_with_params.replace("{" + key + "}", value) + + return url_with_params + + +def remove_suffix(input_string, suffix): + if suffix and input_string.endswith(suffix): + return input_string[: -len(suffix)] + return input_string diff --git a/zod-openapi/sdk/src/openapi/utils/values.py b/zod-openapi/sdk/src/openapi/utils/values.py new file mode 100644 index 0000000..dae01a4 --- /dev/null +++ b/zod-openapi/sdk/src/openapi/utils/values.py @@ -0,0 +1,137 @@ +"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" + +from datetime import datetime +from enum import Enum +from email.message import Message +from functools import partial +import os +from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union, cast + +from httpx import Response +from pydantic import BaseModel +from pydantic.fields import FieldInfo + +from ..types.basemodel import Unset + +from .serializers import marshal_json + +from .metadata import ParamMetadata, find_field_metadata + + +def match_content_type(content_type: str, pattern: str) -> bool: + if pattern in (content_type, "*", "*/*"): + return True + + msg = Message() + msg["content-type"] = content_type + media_type = msg.get_content_type() + + if media_type == pattern: + return True + + parts = media_type.split("/") + if len(parts) == 2: + if pattern in (f"{parts[0]}/*", f"*/{parts[1]}"): + return True + + return False + + +def match_status_codes(status_codes: List[str], status_code: int) -> bool: + if "default" in status_codes: + return True + + for code in status_codes: + if code == str(status_code): + return True + + if code.endswith("XX") and code.startswith(str(status_code)[:1]): + return True + return False + + +T = TypeVar("T") + +def cast_partial(typ): + return partial(cast, typ) + +def get_global_from_env( + value: Optional[T], env_key: str, type_cast: Callable[[str], T] +) -> Optional[T]: + if value is not None: + return value + env_value = os.getenv(env_key) + if env_value is not None: + try: + return type_cast(env_value) + except ValueError: + pass + return None + + +def match_response( + response: Response, code: Union[str, List[str]], content_type: str +) -> bool: + codes = code if isinstance(code, list) else [code] + return match_status_codes(codes, response.status_code) and match_content_type( + response.headers.get("content-type", "application/octet-stream"), content_type + ) + + +def _populate_from_globals( + param_name: str, value: Any, param_metadata_type: type, gbls: Any +) -> Tuple[Any, bool]: + if gbls is None: + return value, False + + if not isinstance(gbls, BaseModel): + raise TypeError("globals must be a pydantic model") + + global_fields: Dict[str, FieldInfo] = gbls.__class__.model_fields + found = False + for name in global_fields: + field = global_fields[name] + if name is not param_name: + continue + + found = True + + if value is not None: + return value, True + + global_value = getattr(gbls, name) + + param_metadata = find_field_metadata(field, param_metadata_type) + if param_metadata is None: + return value, True + + return global_value, True + + return value, found + + +def _val_to_string(val) -> str: + if isinstance(val, bool): + return str(val).lower() + if isinstance(val, datetime): + return str(val.isoformat().replace("+00:00", "Z")) + if isinstance(val, Enum): + return str(val.value) + + return str(val) + + +def _get_serialized_params( + metadata: ParamMetadata, field_name: str, obj: Any, typ: type +) -> Dict[str, str]: + params: Dict[str, str] = {} + + serialization = metadata.serialization + if serialization == "json": + params[field_name] = marshal_json(obj, typ) + + return params + + +def _is_set(value: Any) -> bool: + return value is not None and not isinstance(value, Unset)