From bf3e042810148f277cb6074584e8d977ad3852ae Mon Sep 17 00:00:00 2001 From: Robert Jambrecic Date: Tue, 29 Oct 2024 13:53:25 +0000 Subject: [PATCH 01/17] simplified problematic json added --- examples/openapi/whatsapp_simple.json | 477 +++++++++++++++++++++++++ tests/api/openapi/test_whatsapp_api.py | 7 +- 2 files changed, 482 insertions(+), 2 deletions(-) create mode 100644 examples/openapi/whatsapp_simple.json diff --git a/examples/openapi/whatsapp_simple.json b/examples/openapi/whatsapp_simple.json new file mode 100644 index 00000000..12c72364 --- /dev/null +++ b/examples/openapi/whatsapp_simple.json @@ -0,0 +1,477 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Infobip OpenAPI Specification", + "description": "OpenAPI Specification that contains all public endpoints and webhooks.", + "contact": { + "name": "Infobip support", + "email": "support@infobip.com" + }, + "version": "2.0.504", + "x-generatedAt": "2024-10-25T09:27:29.201404901Z" + }, + "servers": [ + { + "url": "https://api.infobip.com" + } + ], + "tags": [ + { + "name": "channels", + "description": "Create a perfect customer experience by using the channels your customer already use and love.\n", + "x-type": "category", + "x-displayName": "Channels" + }, + { + "name": "whatsapp", + "description": "With 2 billion users, WhatsApp is the most used application worldwide. It enables you to reach more customers, sharing important and timely notifications, as well as provide real-time customer support. Infobip is an official WhatsApp Business solution provider. Send and manage WhatsApp messages over Infobip's WhatsApp API.\n\nTo utilize WhatsApp in combination with other channels, check out [Messages API](https://www.infobip.com/docs/api/platform/messages-api).\n\n[Learn more about WhatsApp channel and use cases](https://www.infobip.com/docs/whatsapp).\n", + "x-type": "product", + "x-displayName": "WhatsApp" + }, + { + "name": "whatsapp-outbound-messages", + "description": "When you send a WhatsApp message to a phone number belonging to an end user's device you are sending an outbound WhatsApp message. \nThere are several types of WhatsApp messages:\n - Template message – use a pre-defined template to send text, images, videos, share location, documents, attach buttons, and configure SMS failover.\n - Free-form text or media – use it only when contacted by the end user within a certain timeframe to send text, images, audio, video, stickers, share location, or contacts.\n - Interactive messages – send a message that your end user can interact with, such as interactive buttons, lists, or product messages.\n", + "x-type": "module", + "x-displayName": "Outbound messages" + }, + { + "name": "whatsapp-template-message", + "description": "", + "x-type": "section", + "x-displayName": "Template Message" + }, + { + "name": "whatsapp-text-and-media-messages", + "description": "", + "x-type": "section", + "x-displayName": "Text and media messages" + }, + { + "name": "send-whatsapp-interactive-messages", + "description": "", + "x-type": "section", + "x-displayName": "Interactive messages" + }, + { + "name": "whatsapp-inbound-messages", + "description": "When the end user sends a WhatsApp from their device to a phone number, they have sent an inbound WhatsApp message. \nThe inbound message is routed to the Infobip Platform and Infobip in turn routes the message to its customer who has registered the WhatsApp sender.\nTypical supporting features you’d use with inbound messages are:\n - Marking messages as read to communicate to the end user that you have read their message.\n - Downloading media and its metadata sent over by the end user.\n", + "x-type": "module", + "x-displayName": "Inbound messages" + }, + { + "name": "whatsapp-receive-inbound-message", + "description": "", + "x-type": "section", + "x-displayName": "Receive inbound message" + }, + { + "name": "whatsapp-get-inbound-media", + "description": "", + "x-type": "section", + "x-displayName": "Get inbound media" + }, + { + "name": "whatsapp-mark-message-as-read", + "description": "", + "x-type": "section", + "x-displayName": "Mark message as read" + }, + { + "name": "whatsapp-message-status-reports", + "description": "Status Reports tell you what happened to the WhatsApp message you sent, whether it was successfully delivered or failed to be delivered, whether it’s been seen. \nStatus Reports can be pushed in real time to a customer's webhook or can be retrieved by an API call. \nLogs provide similar information to Status Reports but are only available to query for 48hrs.\nThere are a few reports you can set up for your WhatsApp messaging:\n - Delivery Reports - In case of a failure, you’ll receive a timestamp with a delivery failure message and a status code indicating the reason behind it.\n - Seen Reports – In case the message has been delivered successfully to the end user, this report will additionally inform you whether the message has been seen.\n - Payments - It provides all updates to your payment transaction in real time. It's also possible to fetch current state of the payment transaction in any time.\n", + "x-type": "module", + "x-displayName": "Message Status Reports" + }, + { + "name": "whatsapp-status-reports", + "description": "", + "x-type": "section", + "x-displayName": "Status Reports" + }, + { + "name": "whatsapp-payments", + "description": "", + "x-type": "section", + "x-displayName": "Payments" + }, + { + "name": "whatsapp-service-management", + "description": "As opposed to free-form messages, template messages can be sent and delivered at any time. \nHere, you can manage your templates, from template registration, retrieving template statuses, to deleting existing templates.\nWith each WhatsApp message, you can send various types of media. Here, you can manage your media and configure additional feature enhancing their functionality.\nMoreover you can fetch quality and business info of your senders.\n", + "x-type": "module", + "x-displayName": "Service Management" + }, + { + "name": "whatsapp-template-management", + "description": "", + "x-type": "section", + "x-displayName": "Template Management" + }, + { + "name": "whatsapp-flow-management", + "description": "", + "x-type": "section", + "x-displayName": "Flow Management" + }, + { + "name": "whatsapp-media-management", + "description": "", + "x-type": "section", + "x-displayName": "Media Management" + }, + { + "name": "whatsapp-sender-management", + "description": "", + "x-type": "section", + "x-displayName": "Sender Management" + }, + { + "name": "whatsapp-bulk-sender-registration", + "description": "", + "x-type": "section", + "x-displayName": "Bulk Sender Registration" + }, + { + "name": "whatsapp-identity-management", + "description": "Set up identity change, an add-on available for senders hosted by Infobip. This is supported for interactive buttons, interactive lists, or an interactive product message. \nIdentity change allows you to increase your security by preventing messages from being sent to unverified end users. \nOnce enabled for a sender, you would receive notifications when an end user's WhatsApp account (MSISDN) has potentially been transferred to a different user. \nWhen a potential identity change has been detected, the outbound traffic towards that end user is blocked until you verify the user outside the channel and acknowledge the change.\nContact your Account Executive for more information.\n", + "x-type": "module", + "x-displayName": "Identity Management" + }, + { + "name": "whatsapp-ad-conversions", + "description": "The Conversions API for Business Messaging enables advertisers to consolidate web, app, physical store, and business messaging events into a single endpoint for Meta. With Infobip, you can submit Purchase or LeadSubmitted conversion events for WhatsApp.\n", + "x-type": "module", + "x-displayName": "Ad Conversions" + } + ], + "paths": { + "/whatsapp/1/message/text": { + "post": { + "tags": [ + "channels", + "whatsapp", + "whatsapp-outbound-messages", + "whatsapp-text-and-media-messages" + ], + "summary": "Send WhatsApp text message", + "description": "Send a text message to a single recipient. Text messages can only be successfully delivered if the recipient has contacted the business within the last 24 hours, otherwise [template message](#channels/whatsapp/send-whatsapp-template-message) should be used.
The API response will not contain the final delivery status, use [Delivery Reports](#channels/whatsapp/receive-whatsapp-delivery-reports) instead.", + "externalDocs": { + "description": "Learn more about WhatsApp channel and use cases", + "url": "https://www.infobip.com/docs/whatsapp" + }, + "operationId": "send-whatsapp-text-message", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.TextMessage" + }, + "examples": { + "Text message": { + "value": { + "from": "441134960000", + "to": "441134960001", + "messageId": "a28dd97c-1ffb-4fcf-99f1-0b557ed381da", + "content": { + "text": "Some text" + }, + "callbackData": "Callback data", + "notifyUrl": "https://www.example.com/whatsapp", + "urlOptions": { + "shortenUrl": true, + "trackClicks": true, + "trackingUrl": "https://example.com/click-report", + "removeProtocol": true, + "customDomain": "example.com" + } + } + }, + "Text message with previewable url": { + "value": { + "from": "441134960000", + "to": "441134960001", + "messageId": "a28dd97c-1ffb-4fcf-99f1-0b557ed381da", + "content": { + "text": "Some text with url: http://example.com", + "previewUrl": true + }, + "callbackData": "Callback data", + "notifyUrl": "https://www.example.com/whatsapp", + "urlOptions": { + "shortenUrl": true, + "trackClicks": true, + "trackingUrl": "https://example.com/click-report", + "removeProtocol": true, + "customDomain": "example.com" + } + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Message accepted for delivery", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.SingleMessageInfo" + }, + "examples": { + "Success Response": { + "value": { + "to": "441134960001", + "messageCount": 1, + "messageId": "a28dd97c-1ffb-4fcf-99f1-0b557ed381da", + "status": { + "groupId": 1, + "groupName": "PENDING", + "id": 7, + "name": "PENDING_ENROUTE", + "description": "Message sent to next instance" + } + } + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.TextMessage": { + "type": "object", + "properties": { + "from": { + "type": "string", + "description": "Registered WhatsApp sender number. Must be in international format and comply with [WhatsApp's requirements](https://www.infobip.com/docs/whatsapp/get-started#phone-number-what-you-need-to-know).", + "maxLength": 24, + "minLength": 1 + }, + "to": { + "type": "string", + "description": "Message recipient number. Must be in international format.", + "maxLength": 24, + "minLength": 1 + }, + "messageId": { + "type": "string", + "description": "The ID that uniquely identifies the message sent.", + "maxLength": 100, + "minLength": 0 + }, + "content": { + "$ref": "#/components/schemas/f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.TextContent" + }, + "callbackData": { + "type": "string", + "description": "Custom client data that will be included in a [Delivery Report](#channels/whatsapp/receive-whatsapp-delivery-reports).", + "maxLength": 4000, + "minLength": 0 + }, + "notifyUrl": { + "type": "string", + "description": "The URL on your callback server to which delivery and seen reports will be sent. [Delivery report format](#channels/whatsapp/receive-whatsapp-delivery-reports), [Seen report format](#channels/whatsapp/receive-whatsapp-seen-reports).", + "maxLength": 2048, + "minLength": 0 + }, + "urlOptions": { + "$ref": "#/components/schemas/f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.UrlOptions" + }, + "entityId": { + "type": "string", + "description": "Required for entity use in a send request for outbound traffic. Returned in notification events. For more details, see our [documentation](https://www.infobip.com/docs/cpaas-x/application-and-entity-management).", + "maxLength": 255, + "minLength": 0 + }, + "applicationId": { + "type": "string", + "description": "Required for application use in a send request for outbound traffic. Returned in notification events. For more details, see our [documentation](https://www.infobip.com/docs/cpaas-x/application-and-entity-management).", + "maxLength": 255, + "minLength": 0 + } + }, + "required": [ + "content", + "from", + "to" + ] + }, + "f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.TextContent": { + "type": "object", + "description": "The content object to build a message that will be sent.", + "properties": { + "text": { + "type": "string", + "description": "Content of the message being sent.", + "maxLength": 4096, + "minLength": 1 + }, + "previewUrl": { + "type": "boolean", + "description": "Allows for URL preview from within the message. If set to `true`, the message content must contain a URL starting with `https://` or `http://`. Defaults to `false`." + } + }, + "required": [ + "text" + ] + }, + "f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.UrlOptions": { + "type": "object", + "description": "Sets up [URL shortening](https://www.infobip.com/docs/url-shortening) and tracking feature.", + "properties": { + "shortenUrl": { + "type": "boolean", + "default": true, + "description": "Enable shortening of the URLs within a message. Set this to `true`, if you want to set up other URL options." + }, + "trackClicks": { + "type": "boolean", + "default": true, + "description": "Enable tracking of short URL clicks within a message: which URL was clicked, how many times, and by whom." + }, + "trackingUrl": { + "type": "string", + "description": "The URL of your callback server on to which the Click report will be sent." + }, + "removeProtocol": { + "type": "boolean", + "default": false, + "description": "Remove a protocol, such as `https://`, from links to shorten a message. Note that some mobiles may not recognize such links as a URL." + }, + "customDomain": { + "type": "string", + "description": "Select a predefined custom domain to use when generating a short URL." + } + } + }, + "f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.SingleMessageInfo": { + "type": "object", + "properties": { + "to": { + "type": "string", + "description": "The destination address of the message.", + "example": "385977666618" + }, + "messageCount": { + "type": "integer", + "format": "int32", + "description": "Number of messages required to deliver.", + "example": 1 + }, + "messageId": { + "type": "string", + "description": "The ID that uniquely identifies the message sent. If not passed, it will be automatically generated and returned in a response.", + "example": "06df139a-7eb5-4a6e-902e-40e892210455" + }, + "status": { + "$ref": "#/components/schemas/f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.SingleMessageStatus" + } + } + }, + "f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.SingleMessageStatus": { + "type": "object", + "description": "Indicates the [status](https://www.infobip.com/docs/essentials/response-status-and-error-codes#api-status-codes) of the message and how to recover from an error should there be any.", + "properties": { + "groupId": { + "type": "integer", + "format": "int32", + "description": "Status group ID.", + "example": 1 + }, + "groupName": { + "type": "string", + "description": "Status group name.", + "example": "PENDING" + }, + "id": { + "type": "integer", + "format": "int32", + "description": "Status ID.", + "example": 7 + }, + "name": { + "type": "string", + "description": "Status name.", + "example": "PENDING_ENROUTE" + }, + "description": { + "type": "string", + "description": "Human-readable description of the status.", + "example": "Message sent to next instance" + }, + "action": { + "type": "string", + "description": "Action that should be taken to eliminate the error." + } + } + }, + "a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMedia": { + "type": "object", + "anyOf": [ + { + "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaImage" + }, + { + "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaVideo" + } + ], + "description": "Media information of included referral.", + "discriminator": { + "propertyName": "type", + "mapping": { + "IMAGE": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaImage", + "VIDEO": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaVideo" + } + }, + "properties": { + "type": { + "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.MediaType" + } + }, + "readOnly": false, + "writeOnly": true + }, + "a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaImage": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.MediaType" + }, + "url": { + "type": "string", + "description": "URL that leads to the image that end user saw and clicked.", + "readOnly": false, + "writeOnly": true + } + } + }, + "a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.MediaType": { + "type": "string", + "enum": [ + "IMAGE", + "VIDEO" + ], + "title": "Type" + }, + "a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaVideo": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.MediaType" + }, + "url": { + "type": "string", + "description": "URL that leads to the video that end user saw and clicked.", + "readOnly": false, + "writeOnly": true + } + } + } + } + } +} diff --git a/tests/api/openapi/test_whatsapp_api.py b/tests/api/openapi/test_whatsapp_api.py index 6060eb32..f5c3f18c 100644 --- a/tests/api/openapi/test_whatsapp_api.py +++ b/tests/api/openapi/test_whatsapp_api.py @@ -16,9 +16,12 @@ def test_real_whatsapp_end2end( mock_response.json.return_value = {"status": "success"} mock_post.return_value = mock_response + # file_name = "whatsapp.json" + file_name = "whatsapp_simple.json" + # file_name = "whatsapp_openapi_complete.json" + file_path = ( - Path(__file__).parent.parent.parent.parent - / "examples/openapi/whatsapp_openapi_complete.json" + Path(__file__).parent.parent.parent.parent / f"examples/openapi/{file_name}" ) with file_path.open(encoding="utf-8") as file: From 2c7e210656903fc77ec7e1867728a369325e30cd Mon Sep 17 00:00:00 2001 From: Robert Jambrecic Date: Tue, 29 Oct 2024 14:32:56 +0000 Subject: [PATCH 02/17] wip --- examples/openapi/whatsapp_simple.json | 2 +- tests/api/openapi/test_whatsapp_api.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/openapi/whatsapp_simple.json b/examples/openapi/whatsapp_simple.json index 12c72364..775c1854 100644 --- a/examples/openapi/whatsapp_simple.json +++ b/examples/openapi/whatsapp_simple.json @@ -474,4 +474,4 @@ } } } -} +} \ No newline at end of file diff --git a/tests/api/openapi/test_whatsapp_api.py b/tests/api/openapi/test_whatsapp_api.py index f5c3f18c..3f6f721d 100644 --- a/tests/api/openapi/test_whatsapp_api.py +++ b/tests/api/openapi/test_whatsapp_api.py @@ -27,7 +27,10 @@ def test_real_whatsapp_end2end( with file_path.open(encoding="utf-8") as file: openapi_json = file.read() - api = OpenAPI.create(openapi_json=openapi_json) + api = OpenAPI.create( + openapi_json=openapi_json, + client_source_path=".", + ) assert isinstance(api, OpenAPI) From 56313753710fcfe795406ea08721de0a5be19e6b Mon Sep 17 00:00:00 2001 From: Robert Jambrecic Date: Wed, 30 Oct 2024 07:56:58 +0000 Subject: [PATCH 03/17] wip --- examples/openapi/whatsapp_simple.json | 22 +- .../whatsapp_simple_discriminator_inside.json | 477 ++++++++++++++++++ tests/api/openapi/test_whatsapp_api.py | 73 +++ 3 files changed, 561 insertions(+), 11 deletions(-) create mode 100644 examples/openapi/whatsapp_simple_discriminator_inside.json diff --git a/examples/openapi/whatsapp_simple.json b/examples/openapi/whatsapp_simple.json index 775c1854..8743a363 100644 --- a/examples/openapi/whatsapp_simple.json +++ b/examples/openapi/whatsapp_simple.json @@ -421,20 +421,20 @@ } ], "description": "Media information of included referral.", - "discriminator": { - "propertyName": "type", - "mapping": { - "IMAGE": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaImage", - "VIDEO": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaVideo" - } - }, "properties": { "type": { "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.MediaType" - } - }, - "readOnly": false, - "writeOnly": true + }, + "discriminator": { + "propertyName": "type", + "mapping": { + "IMAGE": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaImage", + "VIDEO": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaVideo" + } + }, + "readOnly": false, + "writeOnly": true + } }, "a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaImage": { "type": "object", diff --git a/examples/openapi/whatsapp_simple_discriminator_inside.json b/examples/openapi/whatsapp_simple_discriminator_inside.json new file mode 100644 index 00000000..8743a363 --- /dev/null +++ b/examples/openapi/whatsapp_simple_discriminator_inside.json @@ -0,0 +1,477 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Infobip OpenAPI Specification", + "description": "OpenAPI Specification that contains all public endpoints and webhooks.", + "contact": { + "name": "Infobip support", + "email": "support@infobip.com" + }, + "version": "2.0.504", + "x-generatedAt": "2024-10-25T09:27:29.201404901Z" + }, + "servers": [ + { + "url": "https://api.infobip.com" + } + ], + "tags": [ + { + "name": "channels", + "description": "Create a perfect customer experience by using the channels your customer already use and love.\n", + "x-type": "category", + "x-displayName": "Channels" + }, + { + "name": "whatsapp", + "description": "With 2 billion users, WhatsApp is the most used application worldwide. It enables you to reach more customers, sharing important and timely notifications, as well as provide real-time customer support. Infobip is an official WhatsApp Business solution provider. Send and manage WhatsApp messages over Infobip's WhatsApp API.\n\nTo utilize WhatsApp in combination with other channels, check out [Messages API](https://www.infobip.com/docs/api/platform/messages-api).\n\n[Learn more about WhatsApp channel and use cases](https://www.infobip.com/docs/whatsapp).\n", + "x-type": "product", + "x-displayName": "WhatsApp" + }, + { + "name": "whatsapp-outbound-messages", + "description": "When you send a WhatsApp message to a phone number belonging to an end user's device you are sending an outbound WhatsApp message. \nThere are several types of WhatsApp messages:\n - Template message – use a pre-defined template to send text, images, videos, share location, documents, attach buttons, and configure SMS failover.\n - Free-form text or media – use it only when contacted by the end user within a certain timeframe to send text, images, audio, video, stickers, share location, or contacts.\n - Interactive messages – send a message that your end user can interact with, such as interactive buttons, lists, or product messages.\n", + "x-type": "module", + "x-displayName": "Outbound messages" + }, + { + "name": "whatsapp-template-message", + "description": "", + "x-type": "section", + "x-displayName": "Template Message" + }, + { + "name": "whatsapp-text-and-media-messages", + "description": "", + "x-type": "section", + "x-displayName": "Text and media messages" + }, + { + "name": "send-whatsapp-interactive-messages", + "description": "", + "x-type": "section", + "x-displayName": "Interactive messages" + }, + { + "name": "whatsapp-inbound-messages", + "description": "When the end user sends a WhatsApp from their device to a phone number, they have sent an inbound WhatsApp message. \nThe inbound message is routed to the Infobip Platform and Infobip in turn routes the message to its customer who has registered the WhatsApp sender.\nTypical supporting features you’d use with inbound messages are:\n - Marking messages as read to communicate to the end user that you have read their message.\n - Downloading media and its metadata sent over by the end user.\n", + "x-type": "module", + "x-displayName": "Inbound messages" + }, + { + "name": "whatsapp-receive-inbound-message", + "description": "", + "x-type": "section", + "x-displayName": "Receive inbound message" + }, + { + "name": "whatsapp-get-inbound-media", + "description": "", + "x-type": "section", + "x-displayName": "Get inbound media" + }, + { + "name": "whatsapp-mark-message-as-read", + "description": "", + "x-type": "section", + "x-displayName": "Mark message as read" + }, + { + "name": "whatsapp-message-status-reports", + "description": "Status Reports tell you what happened to the WhatsApp message you sent, whether it was successfully delivered or failed to be delivered, whether it’s been seen. \nStatus Reports can be pushed in real time to a customer's webhook or can be retrieved by an API call. \nLogs provide similar information to Status Reports but are only available to query for 48hrs.\nThere are a few reports you can set up for your WhatsApp messaging:\n - Delivery Reports - In case of a failure, you’ll receive a timestamp with a delivery failure message and a status code indicating the reason behind it.\n - Seen Reports – In case the message has been delivered successfully to the end user, this report will additionally inform you whether the message has been seen.\n - Payments - It provides all updates to your payment transaction in real time. It's also possible to fetch current state of the payment transaction in any time.\n", + "x-type": "module", + "x-displayName": "Message Status Reports" + }, + { + "name": "whatsapp-status-reports", + "description": "", + "x-type": "section", + "x-displayName": "Status Reports" + }, + { + "name": "whatsapp-payments", + "description": "", + "x-type": "section", + "x-displayName": "Payments" + }, + { + "name": "whatsapp-service-management", + "description": "As opposed to free-form messages, template messages can be sent and delivered at any time. \nHere, you can manage your templates, from template registration, retrieving template statuses, to deleting existing templates.\nWith each WhatsApp message, you can send various types of media. Here, you can manage your media and configure additional feature enhancing their functionality.\nMoreover you can fetch quality and business info of your senders.\n", + "x-type": "module", + "x-displayName": "Service Management" + }, + { + "name": "whatsapp-template-management", + "description": "", + "x-type": "section", + "x-displayName": "Template Management" + }, + { + "name": "whatsapp-flow-management", + "description": "", + "x-type": "section", + "x-displayName": "Flow Management" + }, + { + "name": "whatsapp-media-management", + "description": "", + "x-type": "section", + "x-displayName": "Media Management" + }, + { + "name": "whatsapp-sender-management", + "description": "", + "x-type": "section", + "x-displayName": "Sender Management" + }, + { + "name": "whatsapp-bulk-sender-registration", + "description": "", + "x-type": "section", + "x-displayName": "Bulk Sender Registration" + }, + { + "name": "whatsapp-identity-management", + "description": "Set up identity change, an add-on available for senders hosted by Infobip. This is supported for interactive buttons, interactive lists, or an interactive product message. \nIdentity change allows you to increase your security by preventing messages from being sent to unverified end users. \nOnce enabled for a sender, you would receive notifications when an end user's WhatsApp account (MSISDN) has potentially been transferred to a different user. \nWhen a potential identity change has been detected, the outbound traffic towards that end user is blocked until you verify the user outside the channel and acknowledge the change.\nContact your Account Executive for more information.\n", + "x-type": "module", + "x-displayName": "Identity Management" + }, + { + "name": "whatsapp-ad-conversions", + "description": "The Conversions API for Business Messaging enables advertisers to consolidate web, app, physical store, and business messaging events into a single endpoint for Meta. With Infobip, you can submit Purchase or LeadSubmitted conversion events for WhatsApp.\n", + "x-type": "module", + "x-displayName": "Ad Conversions" + } + ], + "paths": { + "/whatsapp/1/message/text": { + "post": { + "tags": [ + "channels", + "whatsapp", + "whatsapp-outbound-messages", + "whatsapp-text-and-media-messages" + ], + "summary": "Send WhatsApp text message", + "description": "Send a text message to a single recipient. Text messages can only be successfully delivered if the recipient has contacted the business within the last 24 hours, otherwise [template message](#channels/whatsapp/send-whatsapp-template-message) should be used.
The API response will not contain the final delivery status, use [Delivery Reports](#channels/whatsapp/receive-whatsapp-delivery-reports) instead.", + "externalDocs": { + "description": "Learn more about WhatsApp channel and use cases", + "url": "https://www.infobip.com/docs/whatsapp" + }, + "operationId": "send-whatsapp-text-message", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.TextMessage" + }, + "examples": { + "Text message": { + "value": { + "from": "441134960000", + "to": "441134960001", + "messageId": "a28dd97c-1ffb-4fcf-99f1-0b557ed381da", + "content": { + "text": "Some text" + }, + "callbackData": "Callback data", + "notifyUrl": "https://www.example.com/whatsapp", + "urlOptions": { + "shortenUrl": true, + "trackClicks": true, + "trackingUrl": "https://example.com/click-report", + "removeProtocol": true, + "customDomain": "example.com" + } + } + }, + "Text message with previewable url": { + "value": { + "from": "441134960000", + "to": "441134960001", + "messageId": "a28dd97c-1ffb-4fcf-99f1-0b557ed381da", + "content": { + "text": "Some text with url: http://example.com", + "previewUrl": true + }, + "callbackData": "Callback data", + "notifyUrl": "https://www.example.com/whatsapp", + "urlOptions": { + "shortenUrl": true, + "trackClicks": true, + "trackingUrl": "https://example.com/click-report", + "removeProtocol": true, + "customDomain": "example.com" + } + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Message accepted for delivery", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.SingleMessageInfo" + }, + "examples": { + "Success Response": { + "value": { + "to": "441134960001", + "messageCount": 1, + "messageId": "a28dd97c-1ffb-4fcf-99f1-0b557ed381da", + "status": { + "groupId": 1, + "groupName": "PENDING", + "id": 7, + "name": "PENDING_ENROUTE", + "description": "Message sent to next instance" + } + } + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.TextMessage": { + "type": "object", + "properties": { + "from": { + "type": "string", + "description": "Registered WhatsApp sender number. Must be in international format and comply with [WhatsApp's requirements](https://www.infobip.com/docs/whatsapp/get-started#phone-number-what-you-need-to-know).", + "maxLength": 24, + "minLength": 1 + }, + "to": { + "type": "string", + "description": "Message recipient number. Must be in international format.", + "maxLength": 24, + "minLength": 1 + }, + "messageId": { + "type": "string", + "description": "The ID that uniquely identifies the message sent.", + "maxLength": 100, + "minLength": 0 + }, + "content": { + "$ref": "#/components/schemas/f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.TextContent" + }, + "callbackData": { + "type": "string", + "description": "Custom client data that will be included in a [Delivery Report](#channels/whatsapp/receive-whatsapp-delivery-reports).", + "maxLength": 4000, + "minLength": 0 + }, + "notifyUrl": { + "type": "string", + "description": "The URL on your callback server to which delivery and seen reports will be sent. [Delivery report format](#channels/whatsapp/receive-whatsapp-delivery-reports), [Seen report format](#channels/whatsapp/receive-whatsapp-seen-reports).", + "maxLength": 2048, + "minLength": 0 + }, + "urlOptions": { + "$ref": "#/components/schemas/f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.UrlOptions" + }, + "entityId": { + "type": "string", + "description": "Required for entity use in a send request for outbound traffic. Returned in notification events. For more details, see our [documentation](https://www.infobip.com/docs/cpaas-x/application-and-entity-management).", + "maxLength": 255, + "minLength": 0 + }, + "applicationId": { + "type": "string", + "description": "Required for application use in a send request for outbound traffic. Returned in notification events. For more details, see our [documentation](https://www.infobip.com/docs/cpaas-x/application-and-entity-management).", + "maxLength": 255, + "minLength": 0 + } + }, + "required": [ + "content", + "from", + "to" + ] + }, + "f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.TextContent": { + "type": "object", + "description": "The content object to build a message that will be sent.", + "properties": { + "text": { + "type": "string", + "description": "Content of the message being sent.", + "maxLength": 4096, + "minLength": 1 + }, + "previewUrl": { + "type": "boolean", + "description": "Allows for URL preview from within the message. If set to `true`, the message content must contain a URL starting with `https://` or `http://`. Defaults to `false`." + } + }, + "required": [ + "text" + ] + }, + "f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.UrlOptions": { + "type": "object", + "description": "Sets up [URL shortening](https://www.infobip.com/docs/url-shortening) and tracking feature.", + "properties": { + "shortenUrl": { + "type": "boolean", + "default": true, + "description": "Enable shortening of the URLs within a message. Set this to `true`, if you want to set up other URL options." + }, + "trackClicks": { + "type": "boolean", + "default": true, + "description": "Enable tracking of short URL clicks within a message: which URL was clicked, how many times, and by whom." + }, + "trackingUrl": { + "type": "string", + "description": "The URL of your callback server on to which the Click report will be sent." + }, + "removeProtocol": { + "type": "boolean", + "default": false, + "description": "Remove a protocol, such as `https://`, from links to shorten a message. Note that some mobiles may not recognize such links as a URL." + }, + "customDomain": { + "type": "string", + "description": "Select a predefined custom domain to use when generating a short URL." + } + } + }, + "f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.SingleMessageInfo": { + "type": "object", + "properties": { + "to": { + "type": "string", + "description": "The destination address of the message.", + "example": "385977666618" + }, + "messageCount": { + "type": "integer", + "format": "int32", + "description": "Number of messages required to deliver.", + "example": 1 + }, + "messageId": { + "type": "string", + "description": "The ID that uniquely identifies the message sent. If not passed, it will be automatically generated and returned in a response.", + "example": "06df139a-7eb5-4a6e-902e-40e892210455" + }, + "status": { + "$ref": "#/components/schemas/f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.SingleMessageStatus" + } + } + }, + "f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.SingleMessageStatus": { + "type": "object", + "description": "Indicates the [status](https://www.infobip.com/docs/essentials/response-status-and-error-codes#api-status-codes) of the message and how to recover from an error should there be any.", + "properties": { + "groupId": { + "type": "integer", + "format": "int32", + "description": "Status group ID.", + "example": 1 + }, + "groupName": { + "type": "string", + "description": "Status group name.", + "example": "PENDING" + }, + "id": { + "type": "integer", + "format": "int32", + "description": "Status ID.", + "example": 7 + }, + "name": { + "type": "string", + "description": "Status name.", + "example": "PENDING_ENROUTE" + }, + "description": { + "type": "string", + "description": "Human-readable description of the status.", + "example": "Message sent to next instance" + }, + "action": { + "type": "string", + "description": "Action that should be taken to eliminate the error." + } + } + }, + "a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMedia": { + "type": "object", + "anyOf": [ + { + "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaImage" + }, + { + "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaVideo" + } + ], + "description": "Media information of included referral.", + "properties": { + "type": { + "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.MediaType" + }, + "discriminator": { + "propertyName": "type", + "mapping": { + "IMAGE": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaImage", + "VIDEO": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaVideo" + } + }, + "readOnly": false, + "writeOnly": true + } + }, + "a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaImage": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.MediaType" + }, + "url": { + "type": "string", + "description": "URL that leads to the image that end user saw and clicked.", + "readOnly": false, + "writeOnly": true + } + } + }, + "a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.MediaType": { + "type": "string", + "enum": [ + "IMAGE", + "VIDEO" + ], + "title": "Type" + }, + "a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaVideo": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.MediaType" + }, + "url": { + "type": "string", + "description": "URL that leads to the video that end user saw and clicked.", + "readOnly": false, + "writeOnly": true + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/api/openapi/test_whatsapp_api.py b/tests/api/openapi/test_whatsapp_api.py index 3f6f721d..a6d70c3a 100644 --- a/tests/api/openapi/test_whatsapp_api.py +++ b/tests/api/openapi/test_whatsapp_api.py @@ -81,3 +81,76 @@ def test_real_whatsapp_end2end( "callbackData": "Callback data", }, ) + + +@patch("fastagency.api.openapi.client.requests.post") +def test_real_whatsapp_end2end_problematic( + mock_post: MagicMock, +) -> None: + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"status": "success"} + mock_post.return_value = mock_response + + file_name = "whatsapp_simple.json" + + file_path = ( + Path(__file__).parent.parent.parent.parent / f"examples/openapi/{file_name}" + ) + + with file_path.open(encoding="utf-8") as file: + openapi_json = file.read() + + api = OpenAPI.create( + openapi_json=openapi_json, + client_source_path=".", + ) + + assert isinstance(api, OpenAPI) + + user_proxy = UserProxyAgent( + name="user_proxy", + human_input_mode="NEVER", + code_execution_config=False, + ) + + + functions = ["send_whatsapp_text_message"] + api._register_for_execution(user_proxy, functions=functions) + + assert tuple(user_proxy._function_map.keys()) == ( + "send_whatsapp_text_message", + ) + + send_whatsapp_text_message = user_proxy._function_map[ + "send_whatsapp_text_message" + ] + + send_whatsapp_text_message( + **{ + "body": { + "from": "447860099299", + "to": "38591152131", + "messageId": "test-message-123", + "content": {"text": "Hello, World!"}, + "callbackData": "Callback data", + } + } + ) + + mock_post.assert_called_once() + + mock_post.assert_called_once_with( + "https://api.infobip.com/whatsapp/1/message/text", + params={}, + headers={ + "Content-Type": "application/json", + }, + json={ + "from": "447860099299", + "to": "38591152131", + "messageId": "test-message-123", + "content": {"text": "Hello, World!"}, + "callbackData": "Callback data", + }, + ) From 72aa59776ddd53eb8eeb18dc010523fe2a1c3d7b Mon Sep 17 00:00:00 2001 From: Robert Jambrecic Date: Wed, 30 Oct 2024 08:03:16 +0000 Subject: [PATCH 04/17] wip --- examples/openapi/whatsapp_simple.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/openapi/whatsapp_simple.json b/examples/openapi/whatsapp_simple.json index 8743a363..0bea390b 100644 --- a/examples/openapi/whatsapp_simple.json +++ b/examples/openapi/whatsapp_simple.json @@ -424,17 +424,17 @@ "properties": { "type": { "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.MediaType" - }, - "discriminator": { - "propertyName": "type", - "mapping": { - "IMAGE": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaImage", - "VIDEO": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaVideo" - } - }, - "readOnly": false, - "writeOnly": true - } + } + }, + "discriminator": { + "propertyName": "type", + "mapping": { + "IMAGE": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaImage", + "VIDEO": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaVideo" + } + }, + "readOnly": false, + "writeOnly": true }, "a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaImage": { "type": "object", From 81d0fdd53ac3621355c04dd204b1f983fc86afa6 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Wed, 30 Oct 2024 09:00:38 +0000 Subject: [PATCH 05/17] Add examples --- .../discriminator_inside_properties.json | 63 +++++++++++++++++++ .../discriminator_outside_properties.json | 63 +++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 tests/api/openapi/templates/discriminator_inside_properties.json create mode 100644 tests/api/openapi/templates/discriminator_outside_properties.json diff --git a/tests/api/openapi/templates/discriminator_inside_properties.json b/tests/api/openapi/templates/discriminator_inside_properties.json new file mode 100644 index 00000000..0b227db0 --- /dev/null +++ b/tests/api/openapi/templates/discriminator_inside_properties.json @@ -0,0 +1,63 @@ +{ + "components": { + "schemas": { + "CustomContextVariable": { + "additionalProperties": false, + "oneOf": [ + { + "$ref": "#/components/schemas/UserContextVariable" + } + ], + "properties": { + "type": { + "description": "Type of custom context variable.", + "type": "string" + }, + "discriminator": { + "mapping": { + "issue": "#/components/schemas/IssueContextVariable" + }, + "propertyName": "type" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "IssueContextVariable": { + "description": "An [issue](https://developer.atlassian.com/cloud/jira/platform/jira-expressions-type-reference#issue) specified by ID or key. All the fields of the issue object are available in the Jira expression.", + "properties": { + "id": { + "description": "The issue ID.", + "format": "int64", + "type": "integer" + }, + "key": { + "description": "The issue key.", + "type": "string" + }, + "type": { + "description": "Type of custom context variable.", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + } + }, + "info": { + "title": "discriminator_inside_properties" + }, + "openapi": "3.0.1", + "paths": {}, + "servers": [ + { + "url": "https://your-domain.atlassian.net" + } + ], + "tags": [] +} diff --git a/tests/api/openapi/templates/discriminator_outside_properties.json b/tests/api/openapi/templates/discriminator_outside_properties.json new file mode 100644 index 00000000..13f2ec0f --- /dev/null +++ b/tests/api/openapi/templates/discriminator_outside_properties.json @@ -0,0 +1,63 @@ +{ + "components": { + "schemas": { + "CustomContextVariable": { + "additionalProperties": false, + "discriminator": { + "mapping": { + "issue": "#/components/schemas/IssueContextVariable" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/UserContextVariable" + } + ], + "properties": { + "type": { + "description": "Type of custom context variable.", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "IssueContextVariable": { + "description": "An [issue](https://developer.atlassian.com/cloud/jira/platform/jira-expressions-type-reference#issue) specified by ID or key. All the fields of the issue object are available in the Jira expression.", + "properties": { + "id": { + "description": "The issue ID.", + "format": "int64", + "type": "integer" + }, + "key": { + "description": "The issue key.", + "type": "string" + }, + "type": { + "description": "Type of custom context variable.", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + } + } + }, + "info": { + "title": "discriminator_outside_properties" + }, + "openapi": "3.0.1", + "paths": {}, + "servers": [ + { + "url": "https://your-domain.atlassian.net" + } + ], + "tags": [] +} From bc29c10464f351a6e086c0412e0b7138f894a5b9 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Wed, 30 Oct 2024 14:59:34 +0000 Subject: [PATCH 06/17] WIP: prepare patch for __apply_discriminator_type_patched --- fastagency/api/openapi/__init__.py | 6 + .../openapi/patch_datamodel_code_generator.py | 127 ++++++++++++++++++ ...erties.json => discriminator_in_root.json} | 42 ++++-- ...discriminator_in_root_with_properties.json | 83 ++++++++++++ .../discriminator_inside_properties.json | 49 ++++--- .../test_fastapi_codegen_template.py | 9 +- 6 files changed, 280 insertions(+), 36 deletions(-) create mode 100644 fastagency/api/openapi/patch_datamodel_code_generator.py rename tests/api/openapi/templates/{discriminator_outside_properties.json => discriminator_in_root.json} (74%) create mode 100644 tests/api/openapi/templates/discriminator_in_root_with_properties.json diff --git a/fastagency/api/openapi/__init__.py b/fastagency/api/openapi/__init__.py index 3fbb63ec..b9a9a17d 100644 --- a/fastagency/api/openapi/__init__.py +++ b/fastagency/api/openapi/__init__.py @@ -12,6 +12,12 @@ patch_function_name_parsing() patch_generate_code() +from .patch_datamodel_code_generator import ( + patch_apply_discriminator_type +) + +patch_apply_discriminator_type() + from .client import OpenAPI # noqa: E402 __all__ = ["OpenAPI"] diff --git a/fastagency/api/openapi/patch_datamodel_code_generator.py b/fastagency/api/openapi/patch_datamodel_code_generator.py new file mode 100644 index 00000000..c07a5d8b --- /dev/null +++ b/fastagency/api/openapi/patch_datamodel_code_generator.py @@ -0,0 +1,127 @@ +from typing import List +#from functools import wraps + +from datamodel_code_generator.model import pydantic as pydantic_model +from datamodel_code_generator.model import pydantic_v2 as pydantic_model_v2 +from datamodel_code_generator.imports import ( + IMPORT_LITERAL, + IMPORT_LITERAL_BACKPORT, + Imports, +) + +from datamodel_code_generator.model.base import ( + DataModel, +) + +from datamodel_code_generator.parser.base import Parser + +from ...logging import get_logger + +logger = get_logger(__name__) + +def patch_apply_discriminator_type(): + +# org__apply_discriminator_type = Parser.__apply_discriminator_type + +# @wraps(org__apply_discriminator_type) + def __apply_discriminator_type_patched( + self, + models: List[DataModel], + imports: Imports, + ) -> None: + for model in models: + for field in model.fields: + discriminator = field.extras.get('discriminator') + if not discriminator or not isinstance(discriminator, dict): + continue + property_name = discriminator.get('propertyName') + if not property_name: # pragma: no cover + continue + mapping = discriminator.get('mapping', {}) + for data_type in field.data_type.data_types: + if not data_type.reference: # pragma: no cover + continue + discriminator_model = data_type.reference.source + + if not isinstance( # pragma: no cover + discriminator_model, + (pydantic_model.BaseModel, pydantic_model_v2.BaseModel), + ): + continue # pragma: no cover + + type_names = [] + + def check_paths(model, mapping): + """Helper function to validate paths for a given model.""" + for name, path in mapping.items(): + if model.path.split('#/')[-1] != path.split('#/')[-1]: + if path.startswith('#/') or model.path[:-1] != path.split('/')[-1]: + t_path = path[str(path).find('/') + 1 :] + t_disc = model.path[: str(model.path).find('#')].lstrip('../') + t_disc_2 = '/'.join(t_disc.split('/')[1:]) + if t_path != t_disc and t_path != t_disc_2: + continue + type_names.append(name) + + # Check the main discriminator model path + if mapping: + check_paths(discriminator_model, mapping) + + # Check the base_classes if they exist + for base_class in discriminator_model.base_classes: + if base_class.reference and base_class.reference.path: + check_paths(base_class.reference, mapping) + else: + type_names = [discriminator_model.path.split('/')[-1]] + if not type_names: # pragma: no cover + raise RuntimeError( + f'Discriminator type is not found. {data_type.reference.path}' + ) + has_one_literal = False + for discriminator_field in discriminator_model.fields: + if ( + discriminator_field.original_name + or discriminator_field.name + ) != property_name: + continue + literals = discriminator_field.data_type.literals + if ( + len(literals) == 1 and literals[0] == type_names[0] + if type_names + else None + ): + has_one_literal = True + continue + for ( + field_data_type + ) in discriminator_field.data_type.all_data_types: + if field_data_type.reference: # pragma: no cover + field_data_type.remove_reference() + discriminator_field.data_type = self.data_type( + literals=type_names + ) + discriminator_field.data_type.parent = discriminator_field + discriminator_field.required = True + imports.append(discriminator_field.imports) + has_one_literal = True + if not has_one_literal: + discriminator_model.fields.append( + self.data_model_field_type( + name=property_name, + data_type=self.data_type(literals=type_names), + required=True, + ) + ) + literal = ( + IMPORT_LITERAL + if self.target_python_version.has_literal_type + else IMPORT_LITERAL_BACKPORT + ) + has_imported_literal = any( + literal == import_ for import_ in imports + ) + if has_imported_literal: # pragma: no cover + imports.append(literal) + + Parser.__apply_discriminator_type = __apply_discriminator_type_patched + logger.info("Patched Parser.__apply_discriminator_type") diff --git a/tests/api/openapi/templates/discriminator_outside_properties.json b/tests/api/openapi/templates/discriminator_in_root.json similarity index 74% rename from tests/api/openapi/templates/discriminator_outside_properties.json rename to tests/api/openapi/templates/discriminator_in_root.json index 13f2ec0f..0151aa40 100644 --- a/tests/api/openapi/templates/discriminator_outside_properties.json +++ b/tests/api/openapi/templates/discriminator_in_root.json @@ -3,22 +3,20 @@ "schemas": { "CustomContextVariable": { "additionalProperties": false, - "discriminator": { - "mapping": { - "issue": "#/components/schemas/IssueContextVariable" - }, - "propertyName": "type" - }, "oneOf": [ { "$ref": "#/components/schemas/UserContextVariable" + }, + { + "$ref": "#/components/schemas/IssueContextVariable" } ], - "properties": { - "type": { - "description": "Type of custom context variable.", - "type": "string" - } + "discriminator": { + "mapping": { + "issue": "#/components/schemas/IssueContextVariable", + "user": "#/components/schemas/UserContextVariable" + }, + "propertyName": "type" }, "required": [ "type" @@ -26,7 +24,6 @@ "type": "object" }, "IssueContextVariable": { - "description": "An [issue](https://developer.atlassian.com/cloud/jira/platform/jira-expressions-type-reference#issue) specified by ID or key. All the fields of the issue object are available in the Jira expression.", "properties": { "id": { "description": "The issue ID.", @@ -46,11 +43,28 @@ "type" ], "type": "object" + }, + "UserContextVariable": { + "properties": { + "accountId": { + "description": "The account ID of the user.", + "type": "string" + }, + "type": { + "description": "Type of custom context variable.", + "type": "string" + } + }, + "required": [ + "accountId", + "type" + ], + "type": "object" } } }, "info": { - "title": "discriminator_outside_properties" + "title": "discriminator_in_root" }, "openapi": "3.0.1", "paths": {}, @@ -60,4 +74,4 @@ } ], "tags": [] -} +} \ No newline at end of file diff --git a/tests/api/openapi/templates/discriminator_in_root_with_properties.json b/tests/api/openapi/templates/discriminator_in_root_with_properties.json new file mode 100644 index 00000000..8c368b61 --- /dev/null +++ b/tests/api/openapi/templates/discriminator_in_root_with_properties.json @@ -0,0 +1,83 @@ +{ + "components": { + "schemas": { + "CustomContextVariable": { + "additionalProperties": false, + "oneOf": [ + { + "$ref": "#/components/schemas/UserContextVariable" + }, + { + "$ref": "#/components/schemas/IssueContextVariable" + } + ], + "properties": { + "type": { + "description": "Type of custom context variable.", + "type": "string" + } + }, + "discriminator": { + "mapping": { + "issue": "#/components/schemas/IssueContextVariable", + "user": "#/components/schemas/UserContextVariable" + }, + "propertyName": "type" + }, + "required": [ + "type" + ], + "type": "object" + }, + "IssueContextVariable": { + "properties": { + "id": { + "description": "The issue ID.", + "format": "int64", + "type": "integer" + }, + "key": { + "description": "The issue key.", + "type": "string" + }, + "type": { + "description": "Type of custom context variable.", + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "UserContextVariable": { + "properties": { + "accountId": { + "description": "The account ID of the user.", + "type": "string" + }, + "type": { + "description": "Type of custom context variable.", + "type": "string" + } + }, + "required": [ + "accountId", + "type" + ], + "type": "object" + } + } + }, + "info": { + "title": "discriminator_in_root_with_properties" + }, + "openapi": "3.0.1", + "paths": {}, + "servers": [ + { + "url": "https://your-domain.atlassian.net" + } + ], + "tags": [] +} \ No newline at end of file diff --git a/tests/api/openapi/templates/discriminator_inside_properties.json b/tests/api/openapi/templates/discriminator_inside_properties.json index 0b227db0..133ba672 100644 --- a/tests/api/openapi/templates/discriminator_inside_properties.json +++ b/tests/api/openapi/templates/discriminator_inside_properties.json @@ -2,22 +2,21 @@ "components": { "schemas": { "CustomContextVariable": { - "additionalProperties": false, "oneOf": [ { "$ref": "#/components/schemas/UserContextVariable" + }, + { + "$ref": "#/components/schemas/IssueContextVariable" } ], "properties": { - "type": { - "description": "Type of custom context variable.", - "type": "string" - }, "discriminator": { + "propertyName": "type", "mapping": { - "issue": "#/components/schemas/IssueContextVariable" - }, - "propertyName": "type" + "issue": "#/components/schemas/IssueContextVariable", + "user": "#/components/schemas/UserContextVariable" + } } }, "required": [ @@ -26,38 +25,48 @@ "type": "object" }, "IssueContextVariable": { - "description": "An [issue](https://developer.atlassian.com/cloud/jira/platform/jira-expressions-type-reference#issue) specified by ID or key. All the fields of the issue object are available in the Jira expression.", "properties": { + "type": { + "type": "string" + }, "id": { - "description": "The issue ID.", - "format": "int64", "type": "integer" }, "key": { - "description": "The issue key.", "type": "string" - }, + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "UserContextVariable": { + "properties": { "type": { - "description": "Type of custom context variable.", + "type": "string" + }, + "accountId": { "type": "string" } }, "required": [ - "type" + "type", + "accountId" ], "type": "object" } } }, + "openapi": "3.0.1", "info": { - "title": "discriminator_inside_properties" + "title": "discriminator_inside_properties", + "version": "1.0.0" }, - "openapi": "3.0.1", "paths": {}, "servers": [ { "url": "https://your-domain.atlassian.net" } - ], - "tags": [] -} + ] +} \ No newline at end of file diff --git a/tests/api/openapi/templates/test_fastapi_codegen_template.py b/tests/api/openapi/templates/test_fastapi_codegen_template.py index ab5f09fe..f52e705b 100644 --- a/tests/api/openapi/templates/test_fastapi_codegen_template.py +++ b/tests/api/openapi/templates/test_fastapi_codegen_template.py @@ -5,9 +5,9 @@ import pytest from datamodel_code_generator import DataModelType -from fastapi_code_generator.__main__ import generate_code from fastagency.api.openapi import OpenAPI +from fastapi_code_generator.__main__ import generate_code OPENAPI_FILE_PATHS = list(Path(__file__).parent.glob("*.json")) TEMPLATE_DIR = Path(__file__).parents[4] / "templates" @@ -15,10 +15,15 @@ assert TEMPLATE_DIR.exists(), TEMPLATE_DIR -@pytest.mark.parametrize("openapi_file_path", OPENAPI_FILE_PATHS) +@pytest.mark.parametrize( + "openapi_file_path", + OPENAPI_FILE_PATHS, + ids=[p.name for p in OPENAPI_FILE_PATHS] +) def test_fastapi_codegen_template(openapi_file_path: Path) -> None: with tempfile.TemporaryDirectory() as temp_dir: td = Path(temp_dir) + #td = Path(__file__).parent / openapi_file_path.stem generate_code( input_name=openapi_file_path.name, From 3a123d38fe60c52a88051eedd762ab4e43e6d6a5 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Wed, 30 Oct 2024 15:31:06 +0000 Subject: [PATCH 07/17] WIP: apply patch to topmost class --- .../openapi/patch_datamodel_code_generator.py | 14 +++++++--- tests/api/openapi/test_whatsapp_api.py | 27 +++++++++---------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/fastagency/api/openapi/patch_datamodel_code_generator.py b/fastagency/api/openapi/patch_datamodel_code_generator.py index c07a5d8b..d7d50c4a 100644 --- a/fastagency/api/openapi/patch_datamodel_code_generator.py +++ b/fastagency/api/openapi/patch_datamodel_code_generator.py @@ -13,7 +13,8 @@ DataModel, ) -from datamodel_code_generator.parser.base import Parser +#from datamodel_code_generator.parser import base +from fastapi_code_generator.parser import OpenAPIParser from ...logging import get_logger @@ -123,5 +124,12 @@ def check_paths(model, mapping): if has_imported_literal: # pragma: no cover imports.append(literal) - Parser.__apply_discriminator_type = __apply_discriminator_type_patched - logger.info("Patched Parser.__apply_discriminator_type") + original_name = [name for name in dir(OpenAPIParser) if '__apply_discriminator_type' in name] + if original_name: + # Patch the method using the exact mangled name + setattr(OpenAPIParser, original_name[0], __apply_discriminator_type_patched) + else: + # Handle the case if the method does not exist (unexpected) + raise AttributeError("Method __apply_discriminator_type not found in base.Parser") + + logger.info(f"Patched Parser.__apply_discriminator_type, original name: {original_name}") diff --git a/tests/api/openapi/test_whatsapp_api.py b/tests/api/openapi/test_whatsapp_api.py index d5b377e1..5ecdea87 100644 --- a/tests/api/openapi/test_whatsapp_api.py +++ b/tests/api/openapi/test_whatsapp_api.py @@ -32,32 +32,31 @@ def whatsapp_api_schema() -> str: @patch("fastagency.api.openapi.client.requests.post") def test_real_whatsapp_end2end( mock_post: MagicMock, - whatsapp_api_schema: str, + #whatsapp_api_schema: str, ) -> None: mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"status": "success"} mock_post.return_value = mock_response - # file_name = "whatsapp_simple.json" - - # file_path = ( - # Path(__file__).parent.parent.parent.parent / f"examples/openapi/{file_name}" - # ) + file_name = "whatsapp_simple.json" - # with file_path.open(encoding="utf-8") as file: - # openapi_json = file.read() + file_path = ( + Path(__file__).parent.parent.parent.parent / f"examples/openapi/{file_name}" + ) - # api = OpenAPI.create( - # openapi_json=openapi_json, - # client_source_path=".", - # ) + with file_path.open(encoding="utf-8") as file: + openapi_json = file.read() api = OpenAPI.create( - openapi_json=whatsapp_api_schema, - servers=[{"url": "https://api.infobip.com"}], + openapi_json=openapi_json, ) + # api = OpenAPI.create( + # openapi_json=whatsapp_api_schema, + # servers=[{"url": "https://api.infobip.com"}], + # ) + assert isinstance(api, OpenAPI) header_authorization = "App something" # pragma: allowlist secret From a4332ceb1d210f4b66eab65f9d405cfcac5a1f5a Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Wed, 30 Oct 2024 15:33:29 +0000 Subject: [PATCH 08/17] WIP: apply patch to topmost class --- fastagency/api/openapi/__init__.py | 4 +- .../openapi/patch_datamodel_code_generator.py | 57 +++++++++++-------- .../test_fastapi_codegen_template.py | 8 +-- tests/api/openapi/test_whatsapp_api.py | 4 +- 4 files changed, 39 insertions(+), 34 deletions(-) diff --git a/fastagency/api/openapi/__init__.py b/fastagency/api/openapi/__init__.py index b9a9a17d..5fe962f8 100644 --- a/fastagency/api/openapi/__init__.py +++ b/fastagency/api/openapi/__init__.py @@ -12,9 +12,7 @@ patch_function_name_parsing() patch_generate_code() -from .patch_datamodel_code_generator import ( - patch_apply_discriminator_type -) +from .patch_datamodel_code_generator import patch_apply_discriminator_type patch_apply_discriminator_type() diff --git a/fastagency/api/openapi/patch_datamodel_code_generator.py b/fastagency/api/openapi/patch_datamodel_code_generator.py index d7d50c4a..fed28a35 100644 --- a/fastagency/api/openapi/patch_datamodel_code_generator.py +++ b/fastagency/api/openapi/patch_datamodel_code_generator.py @@ -1,44 +1,42 @@ -from typing import List -#from functools import wraps +# from functools import wraps -from datamodel_code_generator.model import pydantic as pydantic_model -from datamodel_code_generator.model import pydantic_v2 as pydantic_model_v2 from datamodel_code_generator.imports import ( IMPORT_LITERAL, IMPORT_LITERAL_BACKPORT, Imports, ) - +from datamodel_code_generator.model import pydantic as pydantic_model +from datamodel_code_generator.model import pydantic_v2 as pydantic_model_v2 from datamodel_code_generator.model.base import ( DataModel, ) -#from datamodel_code_generator.parser import base +# from datamodel_code_generator.parser import base from fastapi_code_generator.parser import OpenAPIParser from ...logging import get_logger logger = get_logger(__name__) -def patch_apply_discriminator_type(): -# org__apply_discriminator_type = Parser.__apply_discriminator_type +def patch_apply_discriminator_type(): + # org__apply_discriminator_type = Parser.__apply_discriminator_type -# @wraps(org__apply_discriminator_type) + # @wraps(org__apply_discriminator_type) def __apply_discriminator_type_patched( self, - models: List[DataModel], + models: list[DataModel], imports: Imports, ) -> None: for model in models: for field in model.fields: - discriminator = field.extras.get('discriminator') + discriminator = field.extras.get("discriminator") if not discriminator or not isinstance(discriminator, dict): continue - property_name = discriminator.get('propertyName') + property_name = discriminator.get("propertyName") if not property_name: # pragma: no cover continue - mapping = discriminator.get('mapping', {}) + mapping = discriminator.get("mapping", {}) for data_type in field.data_type.data_types: if not data_type.reference: # pragma: no cover continue @@ -55,11 +53,16 @@ def __apply_discriminator_type_patched( def check_paths(model, mapping): """Helper function to validate paths for a given model.""" for name, path in mapping.items(): - if model.path.split('#/')[-1] != path.split('#/')[-1]: - if path.startswith('#/') or model.path[:-1] != path.split('/')[-1]: - t_path = path[str(path).find('/') + 1 :] - t_disc = model.path[: str(model.path).find('#')].lstrip('../') - t_disc_2 = '/'.join(t_disc.split('/')[1:]) + if model.path.split("#/")[-1] != path.split("#/")[-1]: + if ( + path.startswith("#/") + or model.path[:-1] != path.split("/")[-1] + ): + t_path = path[str(path).find("/") + 1 :] + t_disc = model.path[ + : str(model.path).find("#") + ].lstrip("../") + t_disc_2 = "/".join(t_disc.split("/")[1:]) if t_path != t_disc and t_path != t_disc_2: continue type_names.append(name) @@ -73,10 +76,10 @@ def check_paths(model, mapping): if base_class.reference and base_class.reference.path: check_paths(base_class.reference, mapping) else: - type_names = [discriminator_model.path.split('/')[-1]] + type_names = [discriminator_model.path.split("/")[-1]] if not type_names: # pragma: no cover raise RuntimeError( - f'Discriminator type is not found. {data_type.reference.path}' + f"Discriminator type is not found. {data_type.reference.path}" ) has_one_literal = False for discriminator_field in discriminator_model.fields: @@ -124,12 +127,18 @@ def check_paths(model, mapping): if has_imported_literal: # pragma: no cover imports.append(literal) - original_name = [name for name in dir(OpenAPIParser) if '__apply_discriminator_type' in name] + original_name = [ + name for name in dir(OpenAPIParser) if "__apply_discriminator_type" in name + ] if original_name: # Patch the method using the exact mangled name setattr(OpenAPIParser, original_name[0], __apply_discriminator_type_patched) else: # Handle the case if the method does not exist (unexpected) - raise AttributeError("Method __apply_discriminator_type not found in base.Parser") - - logger.info(f"Patched Parser.__apply_discriminator_type, original name: {original_name}") + raise AttributeError( + "Method __apply_discriminator_type not found in base.Parser" + ) + + logger.info( + f"Patched Parser.__apply_discriminator_type, original name: {original_name}" + ) diff --git a/tests/api/openapi/templates/test_fastapi_codegen_template.py b/tests/api/openapi/templates/test_fastapi_codegen_template.py index f52e705b..e1251a50 100644 --- a/tests/api/openapi/templates/test_fastapi_codegen_template.py +++ b/tests/api/openapi/templates/test_fastapi_codegen_template.py @@ -5,9 +5,9 @@ import pytest from datamodel_code_generator import DataModelType +from fastapi_code_generator.__main__ import generate_code from fastagency.api.openapi import OpenAPI -from fastapi_code_generator.__main__ import generate_code OPENAPI_FILE_PATHS = list(Path(__file__).parent.glob("*.json")) TEMPLATE_DIR = Path(__file__).parents[4] / "templates" @@ -16,14 +16,12 @@ @pytest.mark.parametrize( - "openapi_file_path", - OPENAPI_FILE_PATHS, - ids=[p.name for p in OPENAPI_FILE_PATHS] + "openapi_file_path", OPENAPI_FILE_PATHS, ids=[p.name for p in OPENAPI_FILE_PATHS] ) def test_fastapi_codegen_template(openapi_file_path: Path) -> None: with tempfile.TemporaryDirectory() as temp_dir: td = Path(temp_dir) - #td = Path(__file__).parent / openapi_file_path.stem + # td = Path(__file__).parent / openapi_file_path.stem generate_code( input_name=openapi_file_path.name, diff --git a/tests/api/openapi/test_whatsapp_api.py b/tests/api/openapi/test_whatsapp_api.py index 5ecdea87..f1028199 100644 --- a/tests/api/openapi/test_whatsapp_api.py +++ b/tests/api/openapi/test_whatsapp_api.py @@ -1,6 +1,6 @@ from os import environ -from unittest.mock import MagicMock, patch from pathlib import Path +from unittest.mock import MagicMock, patch import pytest import requests @@ -32,7 +32,7 @@ def whatsapp_api_schema() -> str: @patch("fastagency.api.openapi.client.requests.post") def test_real_whatsapp_end2end( mock_post: MagicMock, - #whatsapp_api_schema: str, + # whatsapp_api_schema: str, ) -> None: mock_response = MagicMock() mock_response.status_code = 200 From 5bf121a197f62f0104e7d00631256a4167ef4d8d Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Thu, 31 Oct 2024 08:34:17 +0000 Subject: [PATCH 09/17] Fix whatsapp end2end --- fastagency/api/openapi/client.py | 5 +- main_.py | 42 ++++ models_.py | 181 ++++++++++++++++++ templates/main.jinja2 | 2 +- .../templates/description_with_new_lines.json | 63 ++++++ .../test_fastapi_codegen_template.py | 1 - tests/api/openapi/test_end2end.py | 12 +- tests/api/openapi/test_whatsapp_api.py | 49 +---- 8 files changed, 304 insertions(+), 51 deletions(-) create mode 100644 main_.py create mode 100644 models_.py create mode 100644 tests/api/openapi/templates/description_with_new_lines.json diff --git a/fastagency/api/openapi/client.py b/fastagency/api/openapi/client.py index a9bbf40f..29dd3543 100644 --- a/fastagency/api/openapi/client.py +++ b/fastagency/api/openapi/client.py @@ -170,7 +170,7 @@ def _get_security_params( def _request( self, - method: Literal["put", "get", "post", "head", "delete"], + method: Literal["put", "get", "post", "head", "delete", "patch"], path: str, description: Optional[str] = None, security: Optional[list[BaseSecurity]] = None, @@ -225,6 +225,9 @@ def delete(self, path: str, **kwargs: Any) -> Callable[..., dict[str, Any]]: def head(self, path: str, **kwargs: Any) -> Callable[..., dict[str, Any]]: return self._request("head", path, **kwargs) + + def patch(self, path: str, **kwargs: Any) -> Callable[..., dict[str, Any]]: + return self._request("patch", path, **kwargs) @classmethod def _get_template_dir(cls) -> Path: diff --git a/main_.py b/main_.py new file mode 100644 index 00000000..c5b9595a --- /dev/null +++ b/main_.py @@ -0,0 +1,42 @@ +# generated by fastapi-codegen: +# filename: openapi.json +# timestamp: 2024-10-31T08:32:05+00:00 + +from __future__ import annotations + +from typing import * + +from fastagency.api.openapi import OpenAPI + +from models_ import ( + F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2SingleMessageInfo, + F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2TextMessage, +) + +app = OpenAPI( + title='Infobip OpenAPI Specification', + description='OpenAPI Specification that contains all public endpoints and webhooks.', + contact={'name': 'Infobip support', 'email': 'support@infobip.com'}, + version='2.0.504', + servers=[{'url': 'https://api.infobip.com'}], +) + + +@app.post( + '/whatsapp/1/message/text', + response_model=F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2SingleMessageInfo, + description="""Send a text message to a single recipient. Text messages can only be successfully delivered if the recipient has contacted the business within the last 24 hours, otherwise [template message](#channels/whatsapp/send-whatsapp-template-message) should be used.
The API response will not contain the final delivery status, use [Delivery Reports](#channels/whatsapp/receive-whatsapp-delivery-reports) instead.""", + tags=[ + 'channels', + 'whatsapp', + 'whatsapp-outbound-messages', + 'whatsapp-text-and-media-messages', + ], +) +def send_whatsapp_text_message( + body: F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2TextMessage, +) -> F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2SingleMessageInfo: + """ + Send WhatsApp text message + """ + pass diff --git a/models_.py b/models_.py new file mode 100644 index 00000000..d4b8b43d --- /dev/null +++ b/models_.py @@ -0,0 +1,181 @@ +# generated by fastapi-codegen: +# filename: openapi.json +# timestamp: 2024-10-31T08:32:05+00:00 + +from __future__ import annotations + +from enum import Enum +from typing import Optional, Union + +from pydantic import BaseModel, Field, RootModel, constr +from typing_extensions import Literal + + +class F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2TextContent( + BaseModel +): + text: constr(min_length=1, max_length=4096) = Field( + ..., description='Content of the message being sent.' + ) + previewUrl: Optional[bool] = Field( + None, + description='Allows for URL preview from within the message. If set to `true`, the message content must contain a URL starting with `https://` or `http://`. Defaults to `false`.', + ) + + +class F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2UrlOptions( + BaseModel +): + shortenUrl: Optional[bool] = Field( + True, + description='Enable shortening of the URLs within a message. Set this to `true`, if you want to set up other URL options.', + ) + trackClicks: Optional[bool] = Field( + True, + description='Enable tracking of short URL clicks within a message: which URL was clicked, how many times, and by whom.', + ) + trackingUrl: Optional[str] = Field( + None, + description='The URL of your callback server on to which the Click report will be sent.', + ) + removeProtocol: Optional[bool] = Field( + False, + description='Remove a protocol, such as `https://`, from links to shorten a message. Note that some mobiles may not recognize such links as a URL.', + ) + customDomain: Optional[str] = Field( + None, + description='Select a predefined custom domain to use when generating a short URL.', + ) + + +class F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2SingleMessageStatus( + BaseModel +): + groupId: Optional[int] = Field(None, description='Status group ID.', examples=[1]) + groupName: Optional[str] = Field( + None, description='Status group name.', examples=['PENDING'] + ) + id: Optional[int] = Field(None, description='Status ID.', examples=[7]) + name: Optional[str] = Field( + None, description='Status name.', examples=['PENDING_ENROUTE'] + ) + description: Optional[str] = Field( + None, + description='Human-readable description of the status.', + examples=['Message sent to next instance'], + ) + action: Optional[str] = Field( + None, description='Action that should be taken to eliminate the error.' + ) + + +class A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512MediaType(Enum): + IMAGE = 'IMAGE' + VIDEO = 'VIDEO' + + +class A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMediaVideo( + BaseModel +): + type: Optional[ + A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512MediaType + ] = None + url: Optional[str] = Field( + None, description='URL that leads to the video that end user saw and clicked.' + ) + + +class F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2TextMessage( + BaseModel +): + from_: constr(min_length=1, max_length=24) = Field( + ..., + alias='from', + description="Registered WhatsApp sender number. Must be in international format and comply with [WhatsApp's requirements](https://www.infobip.com/docs/whatsapp/get-started#phone-number-what-you-need-to-know).", + ) + to: constr(min_length=1, max_length=24) = Field( + ..., description='Message recipient number. Must be in international format.' + ) + messageId: Optional[constr(min_length=0, max_length=100)] = Field( + None, description='The ID that uniquely identifies the message sent.' + ) + content: F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2TextContent + callbackData: Optional[constr(min_length=0, max_length=4000)] = Field( + None, + description='Custom client data that will be included in a [Delivery Report](#channels/whatsapp/receive-whatsapp-delivery-reports).', + ) + notifyUrl: Optional[constr(min_length=0, max_length=2048)] = Field( + None, + description='The URL on your callback server to which delivery and seen reports will be sent. [Delivery report format](#channels/whatsapp/receive-whatsapp-delivery-reports), [Seen report format](#channels/whatsapp/receive-whatsapp-seen-reports).', + ) + urlOptions: Optional[ + F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2UrlOptions + ] = None + entityId: Optional[constr(min_length=0, max_length=255)] = Field( + None, + description='Required for entity use in a send request for outbound traffic. Returned in notification events. For more details, see our [documentation](https://www.infobip.com/docs/cpaas-x/application-and-entity-management).', + ) + applicationId: Optional[constr(min_length=0, max_length=255)] = Field( + None, + description='Required for application use in a send request for outbound traffic. Returned in notification events. For more details, see our [documentation](https://www.infobip.com/docs/cpaas-x/application-and-entity-management).', + ) + + +class F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2SingleMessageInfo( + BaseModel +): + to: Optional[str] = Field( + None, + description='The destination address of the message.', + examples=['385977666618'], + ) + messageCount: Optional[int] = Field( + None, description='Number of messages required to deliver.', examples=[1] + ) + messageId: Optional[str] = Field( + None, + description='The ID that uniquely identifies the message sent. If not passed, it will be automatically generated and returned in a response.', + examples=['06df139a-7eb5-4a6e-902e-40e892210455'], + ) + status: Optional[ + F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2SingleMessageStatus + ] = None + + +class A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMedia2( + A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMediaVideo +): + type: Literal['VIDEO'] + + +class A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMediaImage( + BaseModel +): + type: Optional[ + A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512MediaType + ] = None + url: Optional[str] = Field( + None, description='URL that leads to the image that end user saw and clicked.' + ) + + +class A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMedia1( + A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMediaImage +): + type: Literal['IMAGE'] + + +class A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMedia( + RootModel[ + Union[ + A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMedia1, + A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMedia2, + ] + ] +): + root: Union[ + A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMedia1, + A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMedia2, + ] = Field( + ..., description='Media information of included referral.', discriminator='type' + ) diff --git a/templates/main.jinja2 b/templates/main.jinja2 index aaa052df..fdedfdc0 100644 --- a/templates/main.jinja2 +++ b/templates/main.jinja2 @@ -21,7 +21,7 @@ app = OpenAPI( {% for operation in operations %} @app.{{operation.type}}('{{operation.path}}', response_model={{operation.response}} {% if operation.description %} - , description="{{operation.description}}" + , description="""{{operation.description}}""" {% endif %} {% if operation.additional_responses %} , responses={ diff --git a/tests/api/openapi/templates/description_with_new_lines.json b/tests/api/openapi/templates/description_with_new_lines.json new file mode 100644 index 00000000..77fc898b --- /dev/null +++ b/tests/api/openapi/templates/description_with_new_lines.json @@ -0,0 +1,63 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "description_with_new_lines", + "version": "0.1.0" + }, + "servers": [ + { + "url": "http://localhost:8080", + "description": "Local environment" + } + ], + "paths": { + "/items/{item_id}/update": { + "put": { + "summary": "Update Item", + "description": "Updates an item by its ID.\nThis endpoint modifies item details.", + "operationId": "update_item", + "parameters": [ + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Successful Response" + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Error": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Error message describing the issue" + } + }, + "required": [ + "message" + ] + } + } + } +} \ No newline at end of file diff --git a/tests/api/openapi/templates/test_fastapi_codegen_template.py b/tests/api/openapi/templates/test_fastapi_codegen_template.py index e1251a50..d24aa180 100644 --- a/tests/api/openapi/templates/test_fastapi_codegen_template.py +++ b/tests/api/openapi/templates/test_fastapi_codegen_template.py @@ -21,7 +21,6 @@ def test_fastapi_codegen_template(openapi_file_path: Path) -> None: with tempfile.TemporaryDirectory() as temp_dir: td = Path(temp_dir) - # td = Path(__file__).parent / openapi_file_path.stem generate_code( input_name=openapi_file_path.name, diff --git a/tests/api/openapi/test_end2end.py b/tests/api/openapi/test_end2end.py index 17ade6a3..9d705099 100644 --- a/tests/api/openapi/test_end2end.py +++ b/tests/api/openapi/test_end2end.py @@ -419,7 +419,7 @@ def create_item_items__post( @app.get( '/items/{item_id}', response_model=ItemsItemIdGetResponse, - description="Read an item by ID", + description="""Read an item by ID""", responses={'422': {'model': HTTPValidationError}}, ) def read_item_items__item_id__get( @@ -435,7 +435,7 @@ def read_item_items__item_id__get( @app.put( '/items/{item_id}', response_model=ItemsItemIdPutResponse, - description="Update an item by ID", + description="""Update an item by ID""", responses={'422': {'model': HTTPValidationError}}, ) def update_item_items__item_id__put( @@ -451,7 +451,7 @@ def update_item_items__item_id__put( @app.delete( '/items/{item_id}', response_model=ItemsItemIdDeleteResponse, - description="Delete an item by ID", + description="""Delete an item by ID""", responses={'422': {'model': HTTPValidationError}}, ) def delete_item_items__item_id__delete( @@ -510,7 +510,7 @@ def create_item_items__post( @app.get( '/items/{item_id}', response_model=ItemsItemIdGetResponse, - description="Read an item by ID", + description="""Read an item by ID""", responses={'422': {'model': HTTPValidationError}}, ) def read_item_items__item_id__get( @@ -526,7 +526,7 @@ def read_item_items__item_id__get( @app.put( '/items/{item_id}', response_model=ItemsItemIdPutResponse, - description="Update an item by ID", + description="""Update an item by ID""", responses={'422': {'model': HTTPValidationError}}, ) def update_item_items__item_id__put( @@ -541,7 +541,7 @@ def update_item_items__item_id__put( @app.delete( '/items/{item_id}', response_model=ItemsItemIdDeleteResponse, - description="Delete an item by ID", + description="""Delete an item by ID""", responses={'422': {'model': HTTPValidationError}}, ) def delete_item_items__item_id__delete( diff --git a/tests/api/openapi/test_whatsapp_api.py b/tests/api/openapi/test_whatsapp_api.py index f1028199..9da56af2 100644 --- a/tests/api/openapi/test_whatsapp_api.py +++ b/tests/api/openapi/test_whatsapp_api.py @@ -2,61 +2,26 @@ from pathlib import Path from unittest.mock import MagicMock, patch -import pytest -import requests from autogen import UserProxyAgent from fastagency.api.openapi.client import OpenAPI from fastagency.api.openapi.security import APIKeyHeader -@pytest.fixture(scope="session") -def whatsapp_api_schema() -> str: - postman_api_key = environ.get("POSTMAN_API_KEY") - api_id = "348d2a2f-42dc-4e65-86c7-7c4b589a0693" - url = f"https://api.getpostman.com/apis/{api_id}" - - # Define the headers for authentication - headers = {"X-Api-Key": postman_api_key} - - # Make the GET request to download the collection - response = requests.get(url, headers=headers) - - response.raise_for_status() - api_data = response.json() - - return api_data["api"]["versions"][0]["schemas"][0]["content"] # type: ignore [no-any-return] - - -@pytest.mark.postman @patch("fastagency.api.openapi.client.requests.post") def test_real_whatsapp_end2end( mock_post: MagicMock, - # whatsapp_api_schema: str, ) -> None: mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"status": "success"} mock_post.return_value = mock_response - file_name = "whatsapp_simple.json" - - file_path = ( - Path(__file__).parent.parent.parent.parent / f"examples/openapi/{file_name}" - ) - - with file_path.open(encoding="utf-8") as file: - openapi_json = file.read() - api = OpenAPI.create( - openapi_json=openapi_json, + openapi_url="https://dev.infobip.com/openapi/products/whatsapp.json", + servers=[{"url": "https://api.infobip.com"}], ) - # api = OpenAPI.create( - # openapi_json=whatsapp_api_schema, - # servers=[{"url": "https://api.infobip.com"}], - # ) - assert isinstance(api, OpenAPI) header_authorization = "App something" # pragma: allowlist secret @@ -68,18 +33,18 @@ def test_real_whatsapp_end2end( code_execution_config=False, ) - functions = ["channels_whatsapp_send_whatsapp_text_message"] + functions = ["send_whatsapp_text_message"] api._register_for_execution(user_proxy, functions=functions) assert tuple(user_proxy._function_map.keys()) == ( - "channels_whatsapp_send_whatsapp_text_message", + "send_whatsapp_text_message", ) - channels_whatsapp_send_whatsapp_text_message = user_proxy._function_map[ - "channels_whatsapp_send_whatsapp_text_message" + send_whatsapp_text_message = user_proxy._function_map[ + "send_whatsapp_text_message" ] - channels_whatsapp_send_whatsapp_text_message( + send_whatsapp_text_message( **{ "body": { "from": "447860099299", From f46fff4f1ca40cebcd640884b2e15bdf1c9aea27 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Thu, 31 Oct 2024 08:34:36 +0000 Subject: [PATCH 10/17] Fix whatsapp end2end --- fastagency/api/openapi/client.py | 2 +- tests/api/openapi/templates/description_with_new_lines.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fastagency/api/openapi/client.py b/fastagency/api/openapi/client.py index 29dd3543..41f42e34 100644 --- a/fastagency/api/openapi/client.py +++ b/fastagency/api/openapi/client.py @@ -225,7 +225,7 @@ def delete(self, path: str, **kwargs: Any) -> Callable[..., dict[str, Any]]: def head(self, path: str, **kwargs: Any) -> Callable[..., dict[str, Any]]: return self._request("head", path, **kwargs) - + def patch(self, path: str, **kwargs: Any) -> Callable[..., dict[str, Any]]: return self._request("patch", path, **kwargs) diff --git a/tests/api/openapi/templates/description_with_new_lines.json b/tests/api/openapi/templates/description_with_new_lines.json index 77fc898b..fd63da46 100644 --- a/tests/api/openapi/templates/description_with_new_lines.json +++ b/tests/api/openapi/templates/description_with_new_lines.json @@ -60,4 +60,4 @@ } } } -} \ No newline at end of file +} From 1bfbe32cecadc4cf19019b00d2a7c6bb2dd8a343 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Thu, 31 Oct 2024 08:36:50 +0000 Subject: [PATCH 11/17] Remove postman test --- .github/workflows/pipeline.yaml | 12 ++---------- .github/workflows/test.yaml | 23 ++++++----------------- pyproject.toml | 1 - 3 files changed, 8 insertions(+), 28 deletions(-) diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 9cf606ad..1a706026 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -97,14 +97,6 @@ jobs: use-llms: "llm" secrets: inherit # pragma: allowlist secret - test-with-postman: - uses: ./.github/workflows/test.yaml - with: - python-version: "3.9" - environment: testing - use-postman: true - secrets: inherit # pragma: allowlist secret - test-macos-latest: if: github.event.pull_request.draft == false runs-on: macos-latest @@ -121,7 +113,7 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: pip install .[submodules,docs,testing] - name: Test - run: bash scripts/test.sh -m "not (nats or anthropic or azure_oai or openai or togetherai or llm or postman)" + run: bash scripts/test.sh -m "not (nats or anthropic or azure_oai or openai or togetherai or llm)" test-windows-latest: if: github.event.pull_request.draft == false @@ -139,7 +131,7 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: pip install .[submodules,docs,testing] - name: Test - run: bash scripts/test.sh -m "not (nats or anthropic or azure_oai or openai or togetherai or llm or postman)" + run: bash scripts/test.sh -m "not (nats or anthropic or azure_oai or openai or togetherai or llm)" coverage-combine: needs: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 4d02cc78..b18ea1ab 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -17,11 +17,6 @@ on: required: false type: string default: "" - use-postman: - description: 'Use Postman API in the tests' - required: false - type: boolean - default: false jobs: test: @@ -105,23 +100,17 @@ jobs: - name: Install Playwright Browsers run: npx playwright install --with-deps - name: Test without LLMs - if: ${{ inputs.use-llms == '' && !inputs.use-postman }} - run: bash scripts/test.sh -vv -m "not (anthropic or azure_oai or openai or togetherai or llm or postman)" - env: - COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ inputs.python-version }}-${{ inputs.use-llms }}-${{ inputs.use-postman }} - CONTEXT: ${{ runner.os }}-py${{ inputs.python-version }}-${{ inputs.use-llms }}-${{ inputs.use-postman }} - - name: Test with Postman - if: ${{ inputs.use-postman }} - run: bash scripts/test.sh -vv -m postman + if: ${{ inputs.use-llms == ''}} + run: bash scripts/test.sh -vv -m "not (anthropic or azure_oai or openai or togetherai or llm)" env: - COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ inputs.python-version }}-${{ inputs.use-llms }}-${{ inputs.use-postman }} - CONTEXT: ${{ runner.os }}-py${{ inputs.python-version }}-${{ inputs.use-llms }}-${{ inputs.use-postman }} + COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ inputs.python-version }}-${{ inputs.use-llms }} + CONTEXT: ${{ runner.os }}-py${{ inputs.python-version }}-${{ inputs.use-llms }} - name: Test with LLMs if: ${{ inputs.use-llms != '' }} run: bash scripts/test.sh -vv -m "${{ inputs.use-llms }}" env: - COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ inputs.python-version }}-${{ inputs.use-llms }}-${{ inputs.use-postman }} - CONTEXT: ${{ runner.os }}-py${{ inputs.python-version }}-${{ inputs.use-llms }}-${{ inputs.use-postman }} + COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ inputs.python-version }}-${{ inputs.use-llms }} + CONTEXT: ${{ runner.os }}-py${{ inputs.python-version }}-${{ inputs.use-llms }} - name: Run Playwright tests without LLMs if: ${{ inputs.python-version != '3.9' }} run: npx playwright test -c "playwright.llm-sans.config.ts" diff --git a/pyproject.toml b/pyproject.toml index d1400f0f..3d617d31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -268,7 +268,6 @@ markers = [ "togetherai", "llm: mark test for use with LLMs", "flaky: mark test as flaky", - "postman", ] [tool.coverage.run] From 1ea8c23021f2472ae05c0b0372373dc81b293548 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Thu, 31 Oct 2024 08:53:59 +0000 Subject: [PATCH 12/17] Update whatsapp example --- docs/docs_src/tutorials/whatsapp/main.py | 7 +- main_.py | 42 ------ models_.py | 181 ----------------------- tests/api/openapi/test_whatsapp_api.py | 1 - 4 files changed, 5 insertions(+), 226 deletions(-) delete mode 100644 main_.py delete mode 100644 models_.py diff --git a/docs/docs_src/tutorials/whatsapp/main.py b/docs/docs_src/tutorials/whatsapp/main.py index b09a6c0b..5d7b8c87 100644 --- a/docs/docs_src/tutorials/whatsapp/main.py +++ b/docs/docs_src/tutorials/whatsapp/main.py @@ -21,9 +21,11 @@ "temperature": 0.8, } -openapi_url = "https://raw.githubusercontent.com/airtai/fastagency/refs/heads/main/examples/openapi/whatsapp_openapi.json" +openapi_url = "https://dev.infobip.com/openapi/products/whatsapp.json" + whatsapp_api = OpenAPI.create( openapi_url=openapi_url, + servers=[{"url": "https://api.infobip.com"}], ) header_authorization = "App " # pragma: allowlist secret @@ -101,11 +103,12 @@ def present_completed_task_or_ask_question( description="""Present completed task or ask question. If you are presenting a completed task, last message should be a question: 'Do yo need anything else?'""", ) - + wf.register_api( api=whatsapp_api, callers=whatsapp_agent, executors=web_surfer, + functions = ["send_whatsapp_text_message"] ) initial_message = ui.text_input( diff --git a/main_.py b/main_.py deleted file mode 100644 index c5b9595a..00000000 --- a/main_.py +++ /dev/null @@ -1,42 +0,0 @@ -# generated by fastapi-codegen: -# filename: openapi.json -# timestamp: 2024-10-31T08:32:05+00:00 - -from __future__ import annotations - -from typing import * - -from fastagency.api.openapi import OpenAPI - -from models_ import ( - F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2SingleMessageInfo, - F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2TextMessage, -) - -app = OpenAPI( - title='Infobip OpenAPI Specification', - description='OpenAPI Specification that contains all public endpoints and webhooks.', - contact={'name': 'Infobip support', 'email': 'support@infobip.com'}, - version='2.0.504', - servers=[{'url': 'https://api.infobip.com'}], -) - - -@app.post( - '/whatsapp/1/message/text', - response_model=F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2SingleMessageInfo, - description="""Send a text message to a single recipient. Text messages can only be successfully delivered if the recipient has contacted the business within the last 24 hours, otherwise [template message](#channels/whatsapp/send-whatsapp-template-message) should be used.
The API response will not contain the final delivery status, use [Delivery Reports](#channels/whatsapp/receive-whatsapp-delivery-reports) instead.""", - tags=[ - 'channels', - 'whatsapp', - 'whatsapp-outbound-messages', - 'whatsapp-text-and-media-messages', - ], -) -def send_whatsapp_text_message( - body: F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2TextMessage, -) -> F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2SingleMessageInfo: - """ - Send WhatsApp text message - """ - pass diff --git a/models_.py b/models_.py deleted file mode 100644 index d4b8b43d..00000000 --- a/models_.py +++ /dev/null @@ -1,181 +0,0 @@ -# generated by fastapi-codegen: -# filename: openapi.json -# timestamp: 2024-10-31T08:32:05+00:00 - -from __future__ import annotations - -from enum import Enum -from typing import Optional, Union - -from pydantic import BaseModel, Field, RootModel, constr -from typing_extensions import Literal - - -class F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2TextContent( - BaseModel -): - text: constr(min_length=1, max_length=4096) = Field( - ..., description='Content of the message being sent.' - ) - previewUrl: Optional[bool] = Field( - None, - description='Allows for URL preview from within the message. If set to `true`, the message content must contain a URL starting with `https://` or `http://`. Defaults to `false`.', - ) - - -class F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2UrlOptions( - BaseModel -): - shortenUrl: Optional[bool] = Field( - True, - description='Enable shortening of the URLs within a message. Set this to `true`, if you want to set up other URL options.', - ) - trackClicks: Optional[bool] = Field( - True, - description='Enable tracking of short URL clicks within a message: which URL was clicked, how many times, and by whom.', - ) - trackingUrl: Optional[str] = Field( - None, - description='The URL of your callback server on to which the Click report will be sent.', - ) - removeProtocol: Optional[bool] = Field( - False, - description='Remove a protocol, such as `https://`, from links to shorten a message. Note that some mobiles may not recognize such links as a URL.', - ) - customDomain: Optional[str] = Field( - None, - description='Select a predefined custom domain to use when generating a short URL.', - ) - - -class F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2SingleMessageStatus( - BaseModel -): - groupId: Optional[int] = Field(None, description='Status group ID.', examples=[1]) - groupName: Optional[str] = Field( - None, description='Status group name.', examples=['PENDING'] - ) - id: Optional[int] = Field(None, description='Status ID.', examples=[7]) - name: Optional[str] = Field( - None, description='Status name.', examples=['PENDING_ENROUTE'] - ) - description: Optional[str] = Field( - None, - description='Human-readable description of the status.', - examples=['Message sent to next instance'], - ) - action: Optional[str] = Field( - None, description='Action that should be taken to eliminate the error.' - ) - - -class A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512MediaType(Enum): - IMAGE = 'IMAGE' - VIDEO = 'VIDEO' - - -class A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMediaVideo( - BaseModel -): - type: Optional[ - A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512MediaType - ] = None - url: Optional[str] = Field( - None, description='URL that leads to the video that end user saw and clicked.' - ) - - -class F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2TextMessage( - BaseModel -): - from_: constr(min_length=1, max_length=24) = Field( - ..., - alias='from', - description="Registered WhatsApp sender number. Must be in international format and comply with [WhatsApp's requirements](https://www.infobip.com/docs/whatsapp/get-started#phone-number-what-you-need-to-know).", - ) - to: constr(min_length=1, max_length=24) = Field( - ..., description='Message recipient number. Must be in international format.' - ) - messageId: Optional[constr(min_length=0, max_length=100)] = Field( - None, description='The ID that uniquely identifies the message sent.' - ) - content: F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2TextContent - callbackData: Optional[constr(min_length=0, max_length=4000)] = Field( - None, - description='Custom client data that will be included in a [Delivery Report](#channels/whatsapp/receive-whatsapp-delivery-reports).', - ) - notifyUrl: Optional[constr(min_length=0, max_length=2048)] = Field( - None, - description='The URL on your callback server to which delivery and seen reports will be sent. [Delivery report format](#channels/whatsapp/receive-whatsapp-delivery-reports), [Seen report format](#channels/whatsapp/receive-whatsapp-seen-reports).', - ) - urlOptions: Optional[ - F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2UrlOptions - ] = None - entityId: Optional[constr(min_length=0, max_length=255)] = Field( - None, - description='Required for entity use in a send request for outbound traffic. Returned in notification events. For more details, see our [documentation](https://www.infobip.com/docs/cpaas-x/application-and-entity-management).', - ) - applicationId: Optional[constr(min_length=0, max_length=255)] = Field( - None, - description='Required for application use in a send request for outbound traffic. Returned in notification events. For more details, see our [documentation](https://www.infobip.com/docs/cpaas-x/application-and-entity-management).', - ) - - -class F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2SingleMessageInfo( - BaseModel -): - to: Optional[str] = Field( - None, - description='The destination address of the message.', - examples=['385977666618'], - ) - messageCount: Optional[int] = Field( - None, description='Number of messages required to deliver.', examples=[1] - ) - messageId: Optional[str] = Field( - None, - description='The ID that uniquely identifies the message sent. If not passed, it will be automatically generated and returned in a response.', - examples=['06df139a-7eb5-4a6e-902e-40e892210455'], - ) - status: Optional[ - F30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2SingleMessageStatus - ] = None - - -class A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMedia2( - A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMediaVideo -): - type: Literal['VIDEO'] - - -class A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMediaImage( - BaseModel -): - type: Optional[ - A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512MediaType - ] = None - url: Optional[str] = Field( - None, description='URL that leads to the image that end user saw and clicked.' - ) - - -class A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMedia1( - A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMediaImage -): - type: Literal['IMAGE'] - - -class A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMedia( - RootModel[ - Union[ - A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMedia1, - A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMedia2, - ] - ] -): - root: Union[ - A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMedia1, - A4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512ReferralMedia2, - ] = Field( - ..., description='Media information of included referral.', discriminator='type' - ) diff --git a/tests/api/openapi/test_whatsapp_api.py b/tests/api/openapi/test_whatsapp_api.py index 9da56af2..2acc7f76 100644 --- a/tests/api/openapi/test_whatsapp_api.py +++ b/tests/api/openapi/test_whatsapp_api.py @@ -93,7 +93,6 @@ def test_real_whatsapp_end2end_problematic( api = OpenAPI.create( openapi_json=openapi_json, - client_source_path=".", ) assert isinstance(api, OpenAPI) From 558958b43ae051ac316b35ef92507f615262d79d Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Thu, 31 Oct 2024 08:54:17 +0000 Subject: [PATCH 13/17] Update whatsapp example --- docs/docs_src/tutorials/whatsapp/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs_src/tutorials/whatsapp/main.py b/docs/docs_src/tutorials/whatsapp/main.py index 5d7b8c87..422bb137 100644 --- a/docs/docs_src/tutorials/whatsapp/main.py +++ b/docs/docs_src/tutorials/whatsapp/main.py @@ -103,7 +103,7 @@ def present_completed_task_or_ask_question( description="""Present completed task or ask question. If you are presenting a completed task, last message should be a question: 'Do yo need anything else?'""", ) - + wf.register_api( api=whatsapp_api, callers=whatsapp_agent, From 211c3a7f91cfe5f69166ec2eedb28a51778f38ef Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Thu, 31 Oct 2024 09:54:17 +0000 Subject: [PATCH 14/17] Run pre-commit on all files --- .devcontainer/devcontainer.env | 3 -- .devcontainer/devcontainer.json | 3 -- .devcontainer/python-3.11/devcontainer.json | 3 -- .devcontainer/python-3.12/devcontainer.json | 3 -- .devcontainer/python-3.9/devcontainer.json | 3 -- .github/workflows/test.yaml | 3 -- docs/docs/SUMMARY.md | 2 + .../patch_apply_discriminator_type.md | 11 ++++ examples/openapi/whatsapp_simple.json | 2 +- .../whatsapp_simple_discriminator_inside.json | 2 +- fastagency/api/openapi/__init__.py | 2 +- .../openapi/patch_datamodel_code_generator.py | 52 +++++++++++-------- .../templates/discriminator_in_root.json | 2 +- ...discriminator_in_root_with_properties.json | 2 +- .../discriminator_inside_properties.json | 2 +- tests/api/openapi/test_whatsapp_api.py | 9 +--- 16 files changed, 51 insertions(+), 53 deletions(-) create mode 100644 docs/docs/en/api/fastagency/api/openapi/patch_datamodel_code_generator/patch_apply_discriminator_type.md diff --git a/.devcontainer/devcontainer.env b/.devcontainer/devcontainer.env index ba2b243e..a977c02a 100644 --- a/.devcontainer/devcontainer.env +++ b/.devcontainer/devcontainer.env @@ -24,6 +24,3 @@ BING_API_KEY=${BING_API_KEY} # GIPHY key GIPHY_API_KEY=${GIPHY_API_KEY} - -# POSTMAN key -POSTMAN_API_KEY=${POSTMAN_API_KEY} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index eabac0f2..062010c2 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -38,9 +38,6 @@ }, "BING_API_KEY": { "description": "This key is optional. The WebSurfer agent can work without it, but when added, it uses Bing's search and data services to improve information retrieval. You can always set it later as an environment variable in the codespace terminal." - }, - "POSTMAN_API_KEY": { - "description": "This key is optional and only needed if you are testing OpenAPI code. Leave it blank if not required. You can always set it later as an environment variable in the codespace terminal." } }, "shutdownAction": "stopCompose", diff --git a/.devcontainer/python-3.11/devcontainer.json b/.devcontainer/python-3.11/devcontainer.json index 7e94f8fc..ef764fd7 100644 --- a/.devcontainer/python-3.11/devcontainer.json +++ b/.devcontainer/python-3.11/devcontainer.json @@ -37,9 +37,6 @@ }, "BING_API_KEY": { "description": "This key is optional. The WebSurfer agent can work without it, but when added, it uses Bing's search and data services to improve information retrieval. You can always set it later as an environment variable in the codespace terminal." - }, - "POSTMAN_API_KEY": { - "description": "This key is optional and only needed if you are testing OpenAPI code. Leave it blank if not required. You can always set it later as an environment variable in the codespace terminal." } }, "shutdownAction": "stopCompose", diff --git a/.devcontainer/python-3.12/devcontainer.json b/.devcontainer/python-3.12/devcontainer.json index 947363f7..edfa1d1f 100644 --- a/.devcontainer/python-3.12/devcontainer.json +++ b/.devcontainer/python-3.12/devcontainer.json @@ -37,9 +37,6 @@ }, "BING_API_KEY": { "description": "This key is optional. The WebSurfer agent can work without it, but when added, it uses Bing's search and data services to improve information retrieval. You can always set it later as an environment variable in the codespace terminal." - }, - "POSTMAN_API_KEY": { - "description": "This key is optional and only needed if you are testing OpenAPI code. Leave it blank if not required. You can always set it later as an environment variable in the codespace terminal." } }, "shutdownAction": "stopCompose", diff --git a/.devcontainer/python-3.9/devcontainer.json b/.devcontainer/python-3.9/devcontainer.json index 74ac9b6c..e0f90043 100644 --- a/.devcontainer/python-3.9/devcontainer.json +++ b/.devcontainer/python-3.9/devcontainer.json @@ -38,9 +38,6 @@ }, "BING_API_KEY": { "description": "This key is optional. The WebSurfer agent can work without it, but when added, it uses Bing's search and data services to improve information retrieval. You can always set it later as an environment variable in the codespace terminal." - }, - "POSTMAN_API_KEY": { - "description": "This key is optional and only needed if you are testing OpenAPI code. Leave it blank if not required. You can always set it later as an environment variable in the codespace terminal." } }, "shutdownAction": "stopCompose", diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b18ea1ab..094f226a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -70,9 +70,6 @@ jobs: if [ -n "${{ secrets.BING_API_KEY }}" ]; then echo "BING_API_KEY=${{ secrets.BING_API_KEY }}" >> $GITHUB_ENV fi - if [ -n "${{ secrets.POSTMAN_API_KEY }}" ]; then - echo "POSTMAN_API_KEY=${{ secrets.POSTMAN_API_KEY }}" >> $GITHUB_ENV - fi - uses: actions/checkout@v4 - name: Set up Python diff --git a/docs/docs/SUMMARY.md b/docs/docs/SUMMARY.md index 81538181..2046243e 100644 --- a/docs/docs/SUMMARY.md +++ b/docs/docs/SUMMARY.md @@ -67,6 +67,8 @@ search: - fastapi_code_generator_helpers - [ArgumentWithDescription](api/fastagency/api/openapi/fastapi_code_generator_helpers/ArgumentWithDescription.md) - [patch_get_parameter_type](api/fastagency/api/openapi/fastapi_code_generator_helpers/patch_get_parameter_type.md) + - patch_datamodel_code_generator + - [patch_apply_discriminator_type](api/fastagency/api/openapi/patch_datamodel_code_generator/patch_apply_discriminator_type.md) - patch_fastapi_code_generator - [patch_function_name_parsing](api/fastagency/api/openapi/patch_fastapi_code_generator/patch_function_name_parsing.md) - [patch_generate_code](api/fastagency/api/openapi/patch_fastapi_code_generator/patch_generate_code.md) diff --git a/docs/docs/en/api/fastagency/api/openapi/patch_datamodel_code_generator/patch_apply_discriminator_type.md b/docs/docs/en/api/fastagency/api/openapi/patch_datamodel_code_generator/patch_apply_discriminator_type.md new file mode 100644 index 00000000..9b0daa6e --- /dev/null +++ b/docs/docs/en/api/fastagency/api/openapi/patch_datamodel_code_generator/patch_apply_discriminator_type.md @@ -0,0 +1,11 @@ +--- +# 0.5 - API +# 2 - Release +# 3 - Contributing +# 5 - Template Page +# 10 - Default +search: + boost: 0.5 +--- + +::: fastagency.api.openapi.patch_datamodel_code_generator.patch_apply_discriminator_type diff --git a/examples/openapi/whatsapp_simple.json b/examples/openapi/whatsapp_simple.json index 0bea390b..ccc0c82c 100644 --- a/examples/openapi/whatsapp_simple.json +++ b/examples/openapi/whatsapp_simple.json @@ -474,4 +474,4 @@ } } } -} \ No newline at end of file +} diff --git a/examples/openapi/whatsapp_simple_discriminator_inside.json b/examples/openapi/whatsapp_simple_discriminator_inside.json index 8743a363..114f451e 100644 --- a/examples/openapi/whatsapp_simple_discriminator_inside.json +++ b/examples/openapi/whatsapp_simple_discriminator_inside.json @@ -474,4 +474,4 @@ } } } -} \ No newline at end of file +} diff --git a/fastagency/api/openapi/__init__.py b/fastagency/api/openapi/__init__.py index 5fe962f8..cdc96987 100644 --- a/fastagency/api/openapi/__init__.py +++ b/fastagency/api/openapi/__init__.py @@ -12,7 +12,7 @@ patch_function_name_parsing() patch_generate_code() -from .patch_datamodel_code_generator import patch_apply_discriminator_type +from .patch_datamodel_code_generator import patch_apply_discriminator_type # noqa: E402 patch_apply_discriminator_type() diff --git a/fastagency/api/openapi/patch_datamodel_code_generator.py b/fastagency/api/openapi/patch_datamodel_code_generator.py index fed28a35..ed27bdc0 100644 --- a/fastagency/api/openapi/patch_datamodel_code_generator.py +++ b/fastagency/api/openapi/patch_datamodel_code_generator.py @@ -1,4 +1,4 @@ -# from functools import wraps +from typing import Union from datamodel_code_generator.imports import ( IMPORT_LITERAL, @@ -10,6 +10,7 @@ from datamodel_code_generator.model.base import ( DataModel, ) +from datamodel_code_generator.reference import Reference # from datamodel_code_generator.parser import base from fastapi_code_generator.parser import OpenAPIParser @@ -19,12 +20,9 @@ logger = get_logger(__name__) -def patch_apply_discriminator_type(): - # org__apply_discriminator_type = Parser.__apply_discriminator_type - - # @wraps(org__apply_discriminator_type) - def __apply_discriminator_type_patched( - self, +def patch_apply_discriminator_type() -> None: # noqa: C901 + def __apply_discriminator_type_patched( # noqa: C901 + self: OpenAPIParser, models: list[DataModel], imports: Imports, ) -> None: @@ -48,23 +46,32 @@ def __apply_discriminator_type_patched( ): continue # pragma: no cover - type_names = [] + type_names: list[str] = [] - def check_paths(model, mapping): + def check_paths( + model: Union[ + pydantic_model.BaseModel, + pydantic_model_v2.BaseModel, + Reference, + ], + mapping: dict[str, str], + type_names: list[str] = type_names, + ) -> None: """Helper function to validate paths for a given model.""" for name, path in mapping.items(): - if model.path.split("#/")[-1] != path.split("#/")[-1]: - if ( - path.startswith("#/") - or model.path[:-1] != path.split("/")[-1] - ): - t_path = path[str(path).find("/") + 1 :] - t_disc = model.path[ - : str(model.path).find("#") - ].lstrip("../") - t_disc_2 = "/".join(t_disc.split("/")[1:]) - if t_path != t_disc and t_path != t_disc_2: - continue + if ( + model.path.split("#/")[-1] != path.split("#/")[-1] + ) and ( + path.startswith("#/") + or model.path[:-1] != path.split("/")[-1] + ): + t_path = path[str(path).find("/") + 1 :] + t_disc = model.path[: str(model.path).find("#")].lstrip( # noqa: B005 + "../" + ) + t_disc_2 = "/".join(t_disc.split("/")[1:]) + if t_path != t_disc and t_path != t_disc_2: + continue type_names.append(name) # Check the main discriminator model path @@ -122,7 +129,8 @@ def check_paths(model, mapping): else IMPORT_LITERAL_BACKPORT ) has_imported_literal = any( - literal == import_ for import_ in imports + literal == import_ # type: ignore [comparison-overlap] + for import_ in imports ) if has_imported_literal: # pragma: no cover imports.append(literal) diff --git a/tests/api/openapi/templates/discriminator_in_root.json b/tests/api/openapi/templates/discriminator_in_root.json index 0151aa40..ed51cc21 100644 --- a/tests/api/openapi/templates/discriminator_in_root.json +++ b/tests/api/openapi/templates/discriminator_in_root.json @@ -74,4 +74,4 @@ } ], "tags": [] -} \ No newline at end of file +} diff --git a/tests/api/openapi/templates/discriminator_in_root_with_properties.json b/tests/api/openapi/templates/discriminator_in_root_with_properties.json index 8c368b61..51623f88 100644 --- a/tests/api/openapi/templates/discriminator_in_root_with_properties.json +++ b/tests/api/openapi/templates/discriminator_in_root_with_properties.json @@ -80,4 +80,4 @@ } ], "tags": [] -} \ No newline at end of file +} diff --git a/tests/api/openapi/templates/discriminator_inside_properties.json b/tests/api/openapi/templates/discriminator_inside_properties.json index 133ba672..a887daf9 100644 --- a/tests/api/openapi/templates/discriminator_inside_properties.json +++ b/tests/api/openapi/templates/discriminator_inside_properties.json @@ -69,4 +69,4 @@ "url": "https://your-domain.atlassian.net" } ] -} \ No newline at end of file +} diff --git a/tests/api/openapi/test_whatsapp_api.py b/tests/api/openapi/test_whatsapp_api.py index 2acc7f76..2fcefbfe 100644 --- a/tests/api/openapi/test_whatsapp_api.py +++ b/tests/api/openapi/test_whatsapp_api.py @@ -1,4 +1,3 @@ -from os import environ from pathlib import Path from unittest.mock import MagicMock, patch @@ -36,13 +35,9 @@ def test_real_whatsapp_end2end( functions = ["send_whatsapp_text_message"] api._register_for_execution(user_proxy, functions=functions) - assert tuple(user_proxy._function_map.keys()) == ( - "send_whatsapp_text_message", - ) + assert tuple(user_proxy._function_map.keys()) == ("send_whatsapp_text_message",) - send_whatsapp_text_message = user_proxy._function_map[ - "send_whatsapp_text_message" - ] + send_whatsapp_text_message = user_proxy._function_map["send_whatsapp_text_message"] send_whatsapp_text_message( **{ From db72b891b321bef9d750a6456f51669d94f39c7b Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Thu, 31 Oct 2024 10:44:44 +0000 Subject: [PATCH 15/17] Fix docs, remove unnecessary specifications --- docs/docs/en/tutorials/whatsapp/index.md | 21 +- docs/docs_src/tutorials/whatsapp/main.py | 6 +- examples/openapi/whatsapp_openapi.json | 260 ---------- examples/openapi/whatsapp_simple.json | 477 ------------------ .../whatsapp_simple_discriminator_inside.json | 477 ------------------ tests/api/openapi/test_whatsapp_api.py | 68 --- 6 files changed, 14 insertions(+), 1295 deletions(-) delete mode 100644 examples/openapi/whatsapp_openapi.json delete mode 100644 examples/openapi/whatsapp_simple.json delete mode 100644 examples/openapi/whatsapp_simple_discriminator_inside.json diff --git a/docs/docs/en/tutorials/whatsapp/index.md b/docs/docs/en/tutorials/whatsapp/index.md index 7117da61..8733e1e1 100644 --- a/docs/docs/en/tutorials/whatsapp/index.md +++ b/docs/docs/en/tutorials/whatsapp/index.md @@ -4,7 +4,7 @@ In this tutorial, we will explore how to leverage the **FastAgency** framework t 1. [**`WebSurferAgent`**](../../api/fastagency/runtimes/autogen/agents/websurfer/WebSurferAgent.md): A web-scraping agent capable of retrieving relevant content from webpages (learn more [here](../../user-guide/runtimes/autogen/websurfer.md)). -2. **WhatsApp agent** – An agent that interacts with the [Infobip WhatsApp API](https://www.infobip.com/docs/api/channels/whatsapp){target="_blank"} to send WhatsApp messages based on the user’s request. It will be created using the standard [**`ConversableAgent`**](https://microsoft.github.io/autogen/0.2/docs/reference/agentchat/conversable_agent/){target="_blank"} from [AutoGen](https://microsoft.github.io/autogen){target="_blank"} and the [**`OpenAPI`**](../../api/fastagency/api/openapi/OpenAPI.md) object instantiated with an OpenAPI [specification](https://raw.githubusercontent.com/airtai/fastagency/refs/heads/main/examples/openapi/whatsapp_openapi.json){target="_blank"} of Infobip's [REST API](https://www.infobip.com/docs/api/channels/whatsapp){target="_blank"}. +2. **WhatsApp agent** – An agent that interacts with the [Infobip WhatsApp API](https://www.infobip.com/docs/api/channels/whatsapp){target="_blank"} to send WhatsApp messages based on the user’s request. It will be created using the standard [**`ConversableAgent`**](https://microsoft.github.io/autogen/0.2/docs/reference/agentchat/conversable_agent/){target="_blank"} from [AutoGen](https://microsoft.github.io/autogen){target="_blank"} and the [**`OpenAPI`**](../../api/fastagency/api/openapi/OpenAPI.md) object instantiated with an OpenAPI [specification](https://dev.infobip.com/openapi/products/whatsapp.json){target="_blank"} of Infobip's [REST API](https://www.infobip.com/docs/api/channels/whatsapp){target="_blank"}. The chat system will operate between these two agents and the user, allowing them to scrape web content and send the relevant information via WhatsApp, all within a seamless conversation. This tutorial will guide you through setting up these agents, handling user interaction, and ensuring secure API communication. @@ -106,11 +106,11 @@ You can set the API keys in your terminal as an environment variable: Now we will go over each key part of the code, explaining its function and purpose within the FastAgency framework. Understanding these components is crucial for building a dynamic interaction between the user, the [**`WebSurferAgent`**](../../api/fastagency/runtimes/autogen/agents/websurfer/WebSurferAgent.md), and the **WhatsAppAgent**. ### Creating the WhatsApp API Instance -The following lines shows hot to initializes the WhatsApp API by loading the OpenAPI specification from a URL. The OpenAPI spec defines how to interact with the WhatsApp API, including endpoints, parameters, and security details. +The following lines shows how to initializes the WhatsApp API by loading the OpenAPI specification from a URL. The OpenAPI spec defines how to interact with the WhatsApp API, including endpoints, parameters, and security details. Also, we configure the **WhatsApp API** with the __*WHATSAPP_API_KEY*__ using __*set_security_params*__ to authenticate our requests. ```python -{! docs_src/tutorials/whatsapp/main.py [ln:24-31] !} +{! docs_src/tutorials/whatsapp/main.py [ln:24-33] !} ``` For more information, visit [**API Integration User Guide**](../../user-guide/api/index.md){target="_blank"}. @@ -121,14 +121,15 @@ For more information, visit [**API Integration User Guide**](../../user-guide/ap Here, we initialize a new workflow using ***AutoGenWorkflows()*** and register it under the name ***"whatsapp_and_websurfer"***. The ***@wf.register*** decorator registers the function to handle chat flow with security enabled, combining both WhatsAppAgent and WebSurferAgent. ```python -{! docs_src/tutorials/whatsapp/main.py [ln:60-62] !} +{! docs_src/tutorials/whatsapp/main.py [ln:60-64] !} + ... ``` ### Interaction with the user This is a core function used by the **WhatsAppAgent** to either present the task result or ask a follow-up question to the user. The message is wrapped in a ***TextInput*** object, and then ***ui.process_message()*** sends it for user interaction. ```python -{! docs_src/tutorials/whatsapp/main.py [ln:65-76] !} +{! docs_src/tutorials/whatsapp/main.py [ln:68-78] !} ``` ### Creating the WhatsApp and WebSurfer Agents @@ -137,7 +138,7 @@ This is a core function used by the **WhatsAppAgent** to either present the task - [**`WebSurferAgent`**](../../api/fastagency/runtimes/autogen/agents/websurfer/WebSurferAgent.md): The [**`WebSurferAgent`**](../../api/fastagency/runtimes/autogen/agents/websurfer/WebSurferAgent.md) is responsible for scraping web content and passes the retrieved data to the **WhatsAppAgent**. It’s configured with a summarizer to condense web content, which is useful when presenting concise data to the user. For more information, visit [**WebSurfer User Guide**](../../user-guide/runtimes/autogen/websurfer.md). ```python -{! docs_src/tutorials/whatsapp/main.py [ln:77-94] !} +{! docs_src/tutorials/whatsapp/main.py [ln:80-96] !} ``` @@ -146,13 +147,13 @@ This is a core function used by the **WhatsAppAgent** to either present the task The function ***present_completed_task_or_ask_question*** is registered to allow the **WhatsAppAgent** to ask questions or present completed tasks after receiving data from the [**`WebSurferAgent`**](../../api/fastagency/runtimes/autogen/agents/websurfer/WebSurferAgent.md). ```python -{! docs_src/tutorials/whatsapp/main.py [ln:95-103] !} +{! docs_src/tutorials/whatsapp/main.py [ln:98-105] !} ``` We register the WhatsApp API, which allows the **WhatsAppAgent** to handle tasks like suggesting messages that will be sent to the user. ```python -{! docs_src/tutorials/whatsapp/main.py [ln:105-109] !} +{! docs_src/tutorials/whatsapp/main.py [ln:107-112] !} ``` ### Initiating the Chat @@ -162,7 +163,7 @@ We initiate the conversation between the user, [**`WebSurferAgent`**](../../api/ Once the conversation ends, the summary is returned to the user, wrapping up the session. ```python -{! docs_src/tutorials/whatsapp/main.py [ln:117-124] !} +{! docs_src/tutorials/whatsapp/main.py [ln:120-125] !} ``` ### Starting the Application @@ -170,7 +171,7 @@ Once the conversation ends, the summary is returned to the user, wrapping up the The FastAgency app is created, using the registered workflows (**`wf`**) and web-based user interface ([**`MesopUI`**](../../api/fastagency/ui/mesop/MesopUI.md)). This makes the conversation between agents and the user interactive. ```python -{! docs_src/tutorials/whatsapp/main.py [ln:127] !} +{! docs_src/tutorials/whatsapp/main.py [ln:130] !} ``` For more information, visit [**Mesop User Guide**](../../user-guide/ui/mesop/basics.md){target="_blank"}. diff --git a/docs/docs_src/tutorials/whatsapp/main.py b/docs/docs_src/tutorials/whatsapp/main.py index 422bb137..a129de44 100644 --- a/docs/docs_src/tutorials/whatsapp/main.py +++ b/docs/docs_src/tutorials/whatsapp/main.py @@ -7,8 +7,8 @@ from fastagency import UI, FastAgency from fastagency.api.openapi.client import OpenAPI from fastagency.api.openapi.security import APIKeyHeader -from fastagency.runtimes.autogen.agents.websurfer import WebSurferAgent from fastagency.runtimes.autogen import AutoGenWorkflows +from fastagency.runtimes.autogen.agents.websurfer import WebSurferAgent from fastagency.ui.mesop import MesopUI llm_config = { @@ -92,7 +92,7 @@ def present_completed_task_or_ask_question( human_input_mode="NEVER", executor=whatsapp_agent, is_termination_msg=is_termination_msg, - bing_api_key=os.getenv("BING_API_KEY") + bing_api_key=os.getenv("BING_API_KEY"), ) register_function( @@ -108,7 +108,7 @@ def present_completed_task_or_ask_question( api=whatsapp_api, callers=whatsapp_agent, executors=web_surfer, - functions = ["send_whatsapp_text_message"] + functions=["send_whatsapp_text_message"], ) initial_message = ui.text_input( diff --git a/examples/openapi/whatsapp_openapi.json b/examples/openapi/whatsapp_openapi.json deleted file mode 100644 index 7f3f5187..00000000 --- a/examples/openapi/whatsapp_openapi.json +++ /dev/null @@ -1,260 +0,0 @@ -{ - "openapi": "3.0.1", - "servers": [ - { - "url": "https://api.infobip.com" - } - ], - "info": { - "title": "Infobip WHATSAPP OpenApi Specification", - "description": "OpenApi Spec containing WHATSAPP public endpoints for Postman collection purposes.", - "contact": { - "name": "Infobip support", - "email": "support@infobip.com" - }, - "version": "1.0.195", - "x-additionalInfo": { - "title": "Integration essentials and developer toolbox", - "markdown": "# Essentials\n​\n- [Get Infobip account](https://www.infobip.com/contact)\n- [Get API key](https://portal.infobip.com/settings/accounts/api-keys)\n- [Authentication and authorization details](https://www.infobip.com/docs/essentials/api-authentication)\n- [My base URL](https://www.infobip.com/docs/essentials/base-url)\n- [Response Status and Error Codes](https://www.infobip.com/docs/essentials/response-status-and-error-codes)\n- [Supported content types](https://www.infobip.com/docs/essentials/content-types)\n- [Libraries](https://www.infobip.com/docs/sdk)\n- [Demo applications](https://www.infobip.com/docs/essentials/demo-applications)\n- [API service status](https://www.infobip.com/docs/essentials/api-service-status)\n- [Integration best practices](https://www.infobip.com/docs/essentials/integration-best-practices)\n​\n" - }, - "x-homepage": { - "markdown": "### Customer engagement\n> Complete API solutions that will help you drive better outcomes for your customers and business, throughout the customer journey with rich set of APIs for managing People, Flow, Conversations and more.\n>\n> [SEE ALL SOLUTIONS](#customer-engagement)\n​\n### Channels\n> If you require a straight forward connectivity for your existing solutions this is what you are looking for. Implement one-way or two-way communication over any major mobile messaging channel like SMS, Voice, RCS, WhatsApp, Viber and more.\n>\n> [SEE ALL CHANNELS](#channels)\n​\n### Platform and connectivity\n>A powerful set of APIs for managing phone numbers, IoT and more.\n>\n>[SEE ALL](#platform-&-connectivity)\n​\n---\n### What are you working on?\nTake your integration to the next level - explore our [partnership program](https://www.infobip.com/partnership)!\n" - } - }, - "paths": { - "/whatsapp/1/message/text": { - "post": { - "tags": [ - "Send WhatsApp Message" - ], - "summary": "Send WhatsApp text message", - "description": "Send a text message to a single recipient. Text messages can only be successfully delivered, if the recipient has contacted the business within the last 24 hours, otherwise template message should be used.", - "externalDocs": { - "description": "Learn more about WhatsApp channel and use cases", - "url": "https://www.infobip.com/docs/whatsapp" - }, - "operationId": "channels-whatsapp-send-whatsapp-text-message", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/infobipwhatsappstandaloneapiservice_openapi_TextMessage" - }, - "examples": { - "Text message": { - "value": { - "from": "441134960000", - "to": "441134960001", - "messageId": "a28dd97c-1ffb-4fcf-99f1-0b557ed381da", - "content": { - "text": "Some text" - }, - "callbackData": "Callback data" - } - }, - "Text message with previewable url": { - "value": { - "from": "441134960000", - "to": "441134960001", - "messageId": "a28dd97c-1ffb-4fcf-99f1-0b557ed381da", - "content": { - "text": "Some text with url: http://example.com", - "previewUrl": true - }, - "callbackData": "Callback data" - } - } - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Message accepted for delivery.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/infobipwhatsappstandaloneapiservice_openapi_SingleMessageInfo" - }, - "examples": { - "Success Response": { - "value": { - "to": "441134960001", - "messageCount": 1, - "messageId": "a28dd97c-1ffb-4fcf-99f1-0b557ed381da", - "status": { - "groupId": 1, - "groupName": "PENDING", - "id": 7, - "name": "PENDING_ENROUTE", - "description": "Message sent to next instance" - } - } - } - } - } - } - } - }, - "security": [ - { - "APIKeyHeader": [] - } - ], - "x-rate-limits": [ - { - "type": "route", - "implementation": "iam", - "limit": "200/s" - } - ], - "x-additionalInfo": { - "markdown": "" - } - } - } - }, - "components": { - "schemas": { - "infobipwhatsappstandaloneapiservice_openapi_TextMessage": { - "required": [ - "content", - "from", - "to" - ], - "type": "object", - "properties": { - "from": { - "maxLength": 24, - "minLength": 1, - "type": "string", - "description": "Registered WhatsApp sender number. Must be in international format." - }, - "to": { - "maxLength": 24, - "minLength": 1, - "type": "string", - "description": "Message recipient number. Must be in international format." - }, - "messageId": { - "maxLength": 50, - "minLength": 0, - "type": "string", - "description": "The ID that uniquely identifies the message sent." - }, - "content": { - "$ref": "#/components/schemas/infobipwhatsappstandaloneapiservice_openapi_TextContent" - }, - "callbackData": { - "maxLength": 4000, - "minLength": 0, - "type": "string", - "description": "Custom client data that will be included in Delivery Report." - } - } - }, - "infobipwhatsappstandaloneapiservice_openapi_TextContent": { - "required": [ - "text" - ], - "type": "object", - "properties": { - "text": { - "maxLength": 4096, - "minLength": 1, - "type": "string", - "description": "Text of the message that will be sent." - }, - "previewUrl": { - "type": "boolean", - "description": "Allows for URL previews in text messages. If the value is set to `true`, text is expected to contain URL starting with `https://` or `http://`. The default value is `false`." - } - }, - "description": "Content of the message that will be sent." - }, - "infobipwhatsappstandaloneapiservice_openapi_SingleMessageStatus": { - "type": "object", - "properties": { - "groupId": { - "type": "integer", - "description": "Status group ID.", - "format": "int32", - "example": 1 - }, - "groupName": { - "type": "string", - "description": "Status group name.", - "example": "PENDING" - }, - "id": { - "type": "integer", - "description": "Status ID.", - "format": "int32", - "example": 7 - }, - "name": { - "type": "string", - "description": "Status name.", - "example": "PENDING_ENROUTE" - }, - "description": { - "type": "string", - "description": "Human-readable description of the status.", - "example": "Message sent to next instance" - }, - "action": { - "type": "string", - "description": "Action that should be taken to eliminate the error." - } - } - }, - "infobipwhatsappstandaloneapiservice_openapi_SingleMessageInfo": { - "type": "object", - "properties": { - "to": { - "type": "string", - "description": "Message destination.", - "example": "385977666618" - }, - "messageCount": { - "type": "integer", - "description": "Number of messages required to deliver.", - "format": "int32", - "example": 1 - }, - "messageId": { - "type": "string", - "description": "The ID that uniquely identifies the message sent.", - "example": "06df139a-7eb5-4a6e-902e-40e892210455" - }, - "status": { - "$ref": "#/components/schemas/infobipwhatsappstandaloneapiservice_openapi_SingleMessageStatus" - } - } - } - }, - "securitySchemes": { - "APIKeyHeader": { - "type": "apiKey", - "description": "This is the most secure authorization type and the one with the most flexibility.\n\nAPI keys can be generated by calling the dedicated API method. Furthermore, API keys can have a limited scope and cover only some API methods. Lastly, they can be revoked at any time. This range of possibilities makes API keys well suited for separating the API access rights across multiple applications or use cases. Finally, the loss of an API key is easily manageable.\n\nYou can manage your API keys from [GUI](https://portal.infobip.com/settings/accounts/api-keys), or programmatically with [dedicated API](#platform-&-connectivity/settings).\n\nAPI key Authorization header example:\n\n```shell\nAuthorization: App 003026bbc133714df1834b8638bb496e-8f4b3d9a-e931-478d-a994-28a725159ab9\n```\n", - "name": "Authorization", - "in": "header" - } - } - }, - "tags": [ - { - "name": "Send WhatsApp Message", - "description": "" - }, - { - "name": "Receive WhatsApp Message", - "description": "" - }, - { - "name": "Manage WhatsApp", - "description": "" - } - ] -} diff --git a/examples/openapi/whatsapp_simple.json b/examples/openapi/whatsapp_simple.json deleted file mode 100644 index ccc0c82c..00000000 --- a/examples/openapi/whatsapp_simple.json +++ /dev/null @@ -1,477 +0,0 @@ -{ - "openapi": "3.1.0", - "info": { - "title": "Infobip OpenAPI Specification", - "description": "OpenAPI Specification that contains all public endpoints and webhooks.", - "contact": { - "name": "Infobip support", - "email": "support@infobip.com" - }, - "version": "2.0.504", - "x-generatedAt": "2024-10-25T09:27:29.201404901Z" - }, - "servers": [ - { - "url": "https://api.infobip.com" - } - ], - "tags": [ - { - "name": "channels", - "description": "Create a perfect customer experience by using the channels your customer already use and love.\n", - "x-type": "category", - "x-displayName": "Channels" - }, - { - "name": "whatsapp", - "description": "With 2 billion users, WhatsApp is the most used application worldwide. It enables you to reach more customers, sharing important and timely notifications, as well as provide real-time customer support. Infobip is an official WhatsApp Business solution provider. Send and manage WhatsApp messages over Infobip's WhatsApp API.\n\nTo utilize WhatsApp in combination with other channels, check out [Messages API](https://www.infobip.com/docs/api/platform/messages-api).\n\n[Learn more about WhatsApp channel and use cases](https://www.infobip.com/docs/whatsapp).\n", - "x-type": "product", - "x-displayName": "WhatsApp" - }, - { - "name": "whatsapp-outbound-messages", - "description": "When you send a WhatsApp message to a phone number belonging to an end user's device you are sending an outbound WhatsApp message. \nThere are several types of WhatsApp messages:\n - Template message – use a pre-defined template to send text, images, videos, share location, documents, attach buttons, and configure SMS failover.\n - Free-form text or media – use it only when contacted by the end user within a certain timeframe to send text, images, audio, video, stickers, share location, or contacts.\n - Interactive messages – send a message that your end user can interact with, such as interactive buttons, lists, or product messages.\n", - "x-type": "module", - "x-displayName": "Outbound messages" - }, - { - "name": "whatsapp-template-message", - "description": "", - "x-type": "section", - "x-displayName": "Template Message" - }, - { - "name": "whatsapp-text-and-media-messages", - "description": "", - "x-type": "section", - "x-displayName": "Text and media messages" - }, - { - "name": "send-whatsapp-interactive-messages", - "description": "", - "x-type": "section", - "x-displayName": "Interactive messages" - }, - { - "name": "whatsapp-inbound-messages", - "description": "When the end user sends a WhatsApp from their device to a phone number, they have sent an inbound WhatsApp message. \nThe inbound message is routed to the Infobip Platform and Infobip in turn routes the message to its customer who has registered the WhatsApp sender.\nTypical supporting features you’d use with inbound messages are:\n - Marking messages as read to communicate to the end user that you have read their message.\n - Downloading media and its metadata sent over by the end user.\n", - "x-type": "module", - "x-displayName": "Inbound messages" - }, - { - "name": "whatsapp-receive-inbound-message", - "description": "", - "x-type": "section", - "x-displayName": "Receive inbound message" - }, - { - "name": "whatsapp-get-inbound-media", - "description": "", - "x-type": "section", - "x-displayName": "Get inbound media" - }, - { - "name": "whatsapp-mark-message-as-read", - "description": "", - "x-type": "section", - "x-displayName": "Mark message as read" - }, - { - "name": "whatsapp-message-status-reports", - "description": "Status Reports tell you what happened to the WhatsApp message you sent, whether it was successfully delivered or failed to be delivered, whether it’s been seen. \nStatus Reports can be pushed in real time to a customer's webhook or can be retrieved by an API call. \nLogs provide similar information to Status Reports but are only available to query for 48hrs.\nThere are a few reports you can set up for your WhatsApp messaging:\n - Delivery Reports - In case of a failure, you’ll receive a timestamp with a delivery failure message and a status code indicating the reason behind it.\n - Seen Reports – In case the message has been delivered successfully to the end user, this report will additionally inform you whether the message has been seen.\n - Payments - It provides all updates to your payment transaction in real time. It's also possible to fetch current state of the payment transaction in any time.\n", - "x-type": "module", - "x-displayName": "Message Status Reports" - }, - { - "name": "whatsapp-status-reports", - "description": "", - "x-type": "section", - "x-displayName": "Status Reports" - }, - { - "name": "whatsapp-payments", - "description": "", - "x-type": "section", - "x-displayName": "Payments" - }, - { - "name": "whatsapp-service-management", - "description": "As opposed to free-form messages, template messages can be sent and delivered at any time. \nHere, you can manage your templates, from template registration, retrieving template statuses, to deleting existing templates.\nWith each WhatsApp message, you can send various types of media. Here, you can manage your media and configure additional feature enhancing their functionality.\nMoreover you can fetch quality and business info of your senders.\n", - "x-type": "module", - "x-displayName": "Service Management" - }, - { - "name": "whatsapp-template-management", - "description": "", - "x-type": "section", - "x-displayName": "Template Management" - }, - { - "name": "whatsapp-flow-management", - "description": "", - "x-type": "section", - "x-displayName": "Flow Management" - }, - { - "name": "whatsapp-media-management", - "description": "", - "x-type": "section", - "x-displayName": "Media Management" - }, - { - "name": "whatsapp-sender-management", - "description": "", - "x-type": "section", - "x-displayName": "Sender Management" - }, - { - "name": "whatsapp-bulk-sender-registration", - "description": "", - "x-type": "section", - "x-displayName": "Bulk Sender Registration" - }, - { - "name": "whatsapp-identity-management", - "description": "Set up identity change, an add-on available for senders hosted by Infobip. This is supported for interactive buttons, interactive lists, or an interactive product message. \nIdentity change allows you to increase your security by preventing messages from being sent to unverified end users. \nOnce enabled for a sender, you would receive notifications when an end user's WhatsApp account (MSISDN) has potentially been transferred to a different user. \nWhen a potential identity change has been detected, the outbound traffic towards that end user is blocked until you verify the user outside the channel and acknowledge the change.\nContact your Account Executive for more information.\n", - "x-type": "module", - "x-displayName": "Identity Management" - }, - { - "name": "whatsapp-ad-conversions", - "description": "The Conversions API for Business Messaging enables advertisers to consolidate web, app, physical store, and business messaging events into a single endpoint for Meta. With Infobip, you can submit Purchase or LeadSubmitted conversion events for WhatsApp.\n", - "x-type": "module", - "x-displayName": "Ad Conversions" - } - ], - "paths": { - "/whatsapp/1/message/text": { - "post": { - "tags": [ - "channels", - "whatsapp", - "whatsapp-outbound-messages", - "whatsapp-text-and-media-messages" - ], - "summary": "Send WhatsApp text message", - "description": "Send a text message to a single recipient. Text messages can only be successfully delivered if the recipient has contacted the business within the last 24 hours, otherwise [template message](#channels/whatsapp/send-whatsapp-template-message) should be used.
The API response will not contain the final delivery status, use [Delivery Reports](#channels/whatsapp/receive-whatsapp-delivery-reports) instead.", - "externalDocs": { - "description": "Learn more about WhatsApp channel and use cases", - "url": "https://www.infobip.com/docs/whatsapp" - }, - "operationId": "send-whatsapp-text-message", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.TextMessage" - }, - "examples": { - "Text message": { - "value": { - "from": "441134960000", - "to": "441134960001", - "messageId": "a28dd97c-1ffb-4fcf-99f1-0b557ed381da", - "content": { - "text": "Some text" - }, - "callbackData": "Callback data", - "notifyUrl": "https://www.example.com/whatsapp", - "urlOptions": { - "shortenUrl": true, - "trackClicks": true, - "trackingUrl": "https://example.com/click-report", - "removeProtocol": true, - "customDomain": "example.com" - } - } - }, - "Text message with previewable url": { - "value": { - "from": "441134960000", - "to": "441134960001", - "messageId": "a28dd97c-1ffb-4fcf-99f1-0b557ed381da", - "content": { - "text": "Some text with url: http://example.com", - "previewUrl": true - }, - "callbackData": "Callback data", - "notifyUrl": "https://www.example.com/whatsapp", - "urlOptions": { - "shortenUrl": true, - "trackClicks": true, - "trackingUrl": "https://example.com/click-report", - "removeProtocol": true, - "customDomain": "example.com" - } - } - } - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Message accepted for delivery", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.SingleMessageInfo" - }, - "examples": { - "Success Response": { - "value": { - "to": "441134960001", - "messageCount": 1, - "messageId": "a28dd97c-1ffb-4fcf-99f1-0b557ed381da", - "status": { - "groupId": 1, - "groupName": "PENDING", - "id": 7, - "name": "PENDING_ENROUTE", - "description": "Message sent to next instance" - } - } - } - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.TextMessage": { - "type": "object", - "properties": { - "from": { - "type": "string", - "description": "Registered WhatsApp sender number. Must be in international format and comply with [WhatsApp's requirements](https://www.infobip.com/docs/whatsapp/get-started#phone-number-what-you-need-to-know).", - "maxLength": 24, - "minLength": 1 - }, - "to": { - "type": "string", - "description": "Message recipient number. Must be in international format.", - "maxLength": 24, - "minLength": 1 - }, - "messageId": { - "type": "string", - "description": "The ID that uniquely identifies the message sent.", - "maxLength": 100, - "minLength": 0 - }, - "content": { - "$ref": "#/components/schemas/f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.TextContent" - }, - "callbackData": { - "type": "string", - "description": "Custom client data that will be included in a [Delivery Report](#channels/whatsapp/receive-whatsapp-delivery-reports).", - "maxLength": 4000, - "minLength": 0 - }, - "notifyUrl": { - "type": "string", - "description": "The URL on your callback server to which delivery and seen reports will be sent. [Delivery report format](#channels/whatsapp/receive-whatsapp-delivery-reports), [Seen report format](#channels/whatsapp/receive-whatsapp-seen-reports).", - "maxLength": 2048, - "minLength": 0 - }, - "urlOptions": { - "$ref": "#/components/schemas/f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.UrlOptions" - }, - "entityId": { - "type": "string", - "description": "Required for entity use in a send request for outbound traffic. Returned in notification events. For more details, see our [documentation](https://www.infobip.com/docs/cpaas-x/application-and-entity-management).", - "maxLength": 255, - "minLength": 0 - }, - "applicationId": { - "type": "string", - "description": "Required for application use in a send request for outbound traffic. Returned in notification events. For more details, see our [documentation](https://www.infobip.com/docs/cpaas-x/application-and-entity-management).", - "maxLength": 255, - "minLength": 0 - } - }, - "required": [ - "content", - "from", - "to" - ] - }, - "f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.TextContent": { - "type": "object", - "description": "The content object to build a message that will be sent.", - "properties": { - "text": { - "type": "string", - "description": "Content of the message being sent.", - "maxLength": 4096, - "minLength": 1 - }, - "previewUrl": { - "type": "boolean", - "description": "Allows for URL preview from within the message. If set to `true`, the message content must contain a URL starting with `https://` or `http://`. Defaults to `false`." - } - }, - "required": [ - "text" - ] - }, - "f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.UrlOptions": { - "type": "object", - "description": "Sets up [URL shortening](https://www.infobip.com/docs/url-shortening) and tracking feature.", - "properties": { - "shortenUrl": { - "type": "boolean", - "default": true, - "description": "Enable shortening of the URLs within a message. Set this to `true`, if you want to set up other URL options." - }, - "trackClicks": { - "type": "boolean", - "default": true, - "description": "Enable tracking of short URL clicks within a message: which URL was clicked, how many times, and by whom." - }, - "trackingUrl": { - "type": "string", - "description": "The URL of your callback server on to which the Click report will be sent." - }, - "removeProtocol": { - "type": "boolean", - "default": false, - "description": "Remove a protocol, such as `https://`, from links to shorten a message. Note that some mobiles may not recognize such links as a URL." - }, - "customDomain": { - "type": "string", - "description": "Select a predefined custom domain to use when generating a short URL." - } - } - }, - "f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.SingleMessageInfo": { - "type": "object", - "properties": { - "to": { - "type": "string", - "description": "The destination address of the message.", - "example": "385977666618" - }, - "messageCount": { - "type": "integer", - "format": "int32", - "description": "Number of messages required to deliver.", - "example": 1 - }, - "messageId": { - "type": "string", - "description": "The ID that uniquely identifies the message sent. If not passed, it will be automatically generated and returned in a response.", - "example": "06df139a-7eb5-4a6e-902e-40e892210455" - }, - "status": { - "$ref": "#/components/schemas/f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.SingleMessageStatus" - } - } - }, - "f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.SingleMessageStatus": { - "type": "object", - "description": "Indicates the [status](https://www.infobip.com/docs/essentials/response-status-and-error-codes#api-status-codes) of the message and how to recover from an error should there be any.", - "properties": { - "groupId": { - "type": "integer", - "format": "int32", - "description": "Status group ID.", - "example": 1 - }, - "groupName": { - "type": "string", - "description": "Status group name.", - "example": "PENDING" - }, - "id": { - "type": "integer", - "format": "int32", - "description": "Status ID.", - "example": 7 - }, - "name": { - "type": "string", - "description": "Status name.", - "example": "PENDING_ENROUTE" - }, - "description": { - "type": "string", - "description": "Human-readable description of the status.", - "example": "Message sent to next instance" - }, - "action": { - "type": "string", - "description": "Action that should be taken to eliminate the error." - } - } - }, - "a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMedia": { - "type": "object", - "anyOf": [ - { - "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaImage" - }, - { - "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaVideo" - } - ], - "description": "Media information of included referral.", - "properties": { - "type": { - "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.MediaType" - } - }, - "discriminator": { - "propertyName": "type", - "mapping": { - "IMAGE": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaImage", - "VIDEO": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaVideo" - } - }, - "readOnly": false, - "writeOnly": true - }, - "a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaImage": { - "type": "object", - "properties": { - "type": { - "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.MediaType" - }, - "url": { - "type": "string", - "description": "URL that leads to the image that end user saw and clicked.", - "readOnly": false, - "writeOnly": true - } - } - }, - "a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.MediaType": { - "type": "string", - "enum": [ - "IMAGE", - "VIDEO" - ], - "title": "Type" - }, - "a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaVideo": { - "type": "object", - "properties": { - "type": { - "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.MediaType" - }, - "url": { - "type": "string", - "description": "URL that leads to the video that end user saw and clicked.", - "readOnly": false, - "writeOnly": true - } - } - } - } - } -} diff --git a/examples/openapi/whatsapp_simple_discriminator_inside.json b/examples/openapi/whatsapp_simple_discriminator_inside.json deleted file mode 100644 index 114f451e..00000000 --- a/examples/openapi/whatsapp_simple_discriminator_inside.json +++ /dev/null @@ -1,477 +0,0 @@ -{ - "openapi": "3.1.0", - "info": { - "title": "Infobip OpenAPI Specification", - "description": "OpenAPI Specification that contains all public endpoints and webhooks.", - "contact": { - "name": "Infobip support", - "email": "support@infobip.com" - }, - "version": "2.0.504", - "x-generatedAt": "2024-10-25T09:27:29.201404901Z" - }, - "servers": [ - { - "url": "https://api.infobip.com" - } - ], - "tags": [ - { - "name": "channels", - "description": "Create a perfect customer experience by using the channels your customer already use and love.\n", - "x-type": "category", - "x-displayName": "Channels" - }, - { - "name": "whatsapp", - "description": "With 2 billion users, WhatsApp is the most used application worldwide. It enables you to reach more customers, sharing important and timely notifications, as well as provide real-time customer support. Infobip is an official WhatsApp Business solution provider. Send and manage WhatsApp messages over Infobip's WhatsApp API.\n\nTo utilize WhatsApp in combination with other channels, check out [Messages API](https://www.infobip.com/docs/api/platform/messages-api).\n\n[Learn more about WhatsApp channel and use cases](https://www.infobip.com/docs/whatsapp).\n", - "x-type": "product", - "x-displayName": "WhatsApp" - }, - { - "name": "whatsapp-outbound-messages", - "description": "When you send a WhatsApp message to a phone number belonging to an end user's device you are sending an outbound WhatsApp message. \nThere are several types of WhatsApp messages:\n - Template message – use a pre-defined template to send text, images, videos, share location, documents, attach buttons, and configure SMS failover.\n - Free-form text or media – use it only when contacted by the end user within a certain timeframe to send text, images, audio, video, stickers, share location, or contacts.\n - Interactive messages – send a message that your end user can interact with, such as interactive buttons, lists, or product messages.\n", - "x-type": "module", - "x-displayName": "Outbound messages" - }, - { - "name": "whatsapp-template-message", - "description": "", - "x-type": "section", - "x-displayName": "Template Message" - }, - { - "name": "whatsapp-text-and-media-messages", - "description": "", - "x-type": "section", - "x-displayName": "Text and media messages" - }, - { - "name": "send-whatsapp-interactive-messages", - "description": "", - "x-type": "section", - "x-displayName": "Interactive messages" - }, - { - "name": "whatsapp-inbound-messages", - "description": "When the end user sends a WhatsApp from their device to a phone number, they have sent an inbound WhatsApp message. \nThe inbound message is routed to the Infobip Platform and Infobip in turn routes the message to its customer who has registered the WhatsApp sender.\nTypical supporting features you’d use with inbound messages are:\n - Marking messages as read to communicate to the end user that you have read their message.\n - Downloading media and its metadata sent over by the end user.\n", - "x-type": "module", - "x-displayName": "Inbound messages" - }, - { - "name": "whatsapp-receive-inbound-message", - "description": "", - "x-type": "section", - "x-displayName": "Receive inbound message" - }, - { - "name": "whatsapp-get-inbound-media", - "description": "", - "x-type": "section", - "x-displayName": "Get inbound media" - }, - { - "name": "whatsapp-mark-message-as-read", - "description": "", - "x-type": "section", - "x-displayName": "Mark message as read" - }, - { - "name": "whatsapp-message-status-reports", - "description": "Status Reports tell you what happened to the WhatsApp message you sent, whether it was successfully delivered or failed to be delivered, whether it’s been seen. \nStatus Reports can be pushed in real time to a customer's webhook or can be retrieved by an API call. \nLogs provide similar information to Status Reports but are only available to query for 48hrs.\nThere are a few reports you can set up for your WhatsApp messaging:\n - Delivery Reports - In case of a failure, you’ll receive a timestamp with a delivery failure message and a status code indicating the reason behind it.\n - Seen Reports – In case the message has been delivered successfully to the end user, this report will additionally inform you whether the message has been seen.\n - Payments - It provides all updates to your payment transaction in real time. It's also possible to fetch current state of the payment transaction in any time.\n", - "x-type": "module", - "x-displayName": "Message Status Reports" - }, - { - "name": "whatsapp-status-reports", - "description": "", - "x-type": "section", - "x-displayName": "Status Reports" - }, - { - "name": "whatsapp-payments", - "description": "", - "x-type": "section", - "x-displayName": "Payments" - }, - { - "name": "whatsapp-service-management", - "description": "As opposed to free-form messages, template messages can be sent and delivered at any time. \nHere, you can manage your templates, from template registration, retrieving template statuses, to deleting existing templates.\nWith each WhatsApp message, you can send various types of media. Here, you can manage your media and configure additional feature enhancing their functionality.\nMoreover you can fetch quality and business info of your senders.\n", - "x-type": "module", - "x-displayName": "Service Management" - }, - { - "name": "whatsapp-template-management", - "description": "", - "x-type": "section", - "x-displayName": "Template Management" - }, - { - "name": "whatsapp-flow-management", - "description": "", - "x-type": "section", - "x-displayName": "Flow Management" - }, - { - "name": "whatsapp-media-management", - "description": "", - "x-type": "section", - "x-displayName": "Media Management" - }, - { - "name": "whatsapp-sender-management", - "description": "", - "x-type": "section", - "x-displayName": "Sender Management" - }, - { - "name": "whatsapp-bulk-sender-registration", - "description": "", - "x-type": "section", - "x-displayName": "Bulk Sender Registration" - }, - { - "name": "whatsapp-identity-management", - "description": "Set up identity change, an add-on available for senders hosted by Infobip. This is supported for interactive buttons, interactive lists, or an interactive product message. \nIdentity change allows you to increase your security by preventing messages from being sent to unverified end users. \nOnce enabled for a sender, you would receive notifications when an end user's WhatsApp account (MSISDN) has potentially been transferred to a different user. \nWhen a potential identity change has been detected, the outbound traffic towards that end user is blocked until you verify the user outside the channel and acknowledge the change.\nContact your Account Executive for more information.\n", - "x-type": "module", - "x-displayName": "Identity Management" - }, - { - "name": "whatsapp-ad-conversions", - "description": "The Conversions API for Business Messaging enables advertisers to consolidate web, app, physical store, and business messaging events into a single endpoint for Meta. With Infobip, you can submit Purchase or LeadSubmitted conversion events for WhatsApp.\n", - "x-type": "module", - "x-displayName": "Ad Conversions" - } - ], - "paths": { - "/whatsapp/1/message/text": { - "post": { - "tags": [ - "channels", - "whatsapp", - "whatsapp-outbound-messages", - "whatsapp-text-and-media-messages" - ], - "summary": "Send WhatsApp text message", - "description": "Send a text message to a single recipient. Text messages can only be successfully delivered if the recipient has contacted the business within the last 24 hours, otherwise [template message](#channels/whatsapp/send-whatsapp-template-message) should be used.
The API response will not contain the final delivery status, use [Delivery Reports](#channels/whatsapp/receive-whatsapp-delivery-reports) instead.", - "externalDocs": { - "description": "Learn more about WhatsApp channel and use cases", - "url": "https://www.infobip.com/docs/whatsapp" - }, - "operationId": "send-whatsapp-text-message", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.TextMessage" - }, - "examples": { - "Text message": { - "value": { - "from": "441134960000", - "to": "441134960001", - "messageId": "a28dd97c-1ffb-4fcf-99f1-0b557ed381da", - "content": { - "text": "Some text" - }, - "callbackData": "Callback data", - "notifyUrl": "https://www.example.com/whatsapp", - "urlOptions": { - "shortenUrl": true, - "trackClicks": true, - "trackingUrl": "https://example.com/click-report", - "removeProtocol": true, - "customDomain": "example.com" - } - } - }, - "Text message with previewable url": { - "value": { - "from": "441134960000", - "to": "441134960001", - "messageId": "a28dd97c-1ffb-4fcf-99f1-0b557ed381da", - "content": { - "text": "Some text with url: http://example.com", - "previewUrl": true - }, - "callbackData": "Callback data", - "notifyUrl": "https://www.example.com/whatsapp", - "urlOptions": { - "shortenUrl": true, - "trackClicks": true, - "trackingUrl": "https://example.com/click-report", - "removeProtocol": true, - "customDomain": "example.com" - } - } - } - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Message accepted for delivery", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.SingleMessageInfo" - }, - "examples": { - "Success Response": { - "value": { - "to": "441134960001", - "messageCount": 1, - "messageId": "a28dd97c-1ffb-4fcf-99f1-0b557ed381da", - "status": { - "groupId": 1, - "groupName": "PENDING", - "id": 7, - "name": "PENDING_ENROUTE", - "description": "Message sent to next instance" - } - } - } - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.TextMessage": { - "type": "object", - "properties": { - "from": { - "type": "string", - "description": "Registered WhatsApp sender number. Must be in international format and comply with [WhatsApp's requirements](https://www.infobip.com/docs/whatsapp/get-started#phone-number-what-you-need-to-know).", - "maxLength": 24, - "minLength": 1 - }, - "to": { - "type": "string", - "description": "Message recipient number. Must be in international format.", - "maxLength": 24, - "minLength": 1 - }, - "messageId": { - "type": "string", - "description": "The ID that uniquely identifies the message sent.", - "maxLength": 100, - "minLength": 0 - }, - "content": { - "$ref": "#/components/schemas/f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.TextContent" - }, - "callbackData": { - "type": "string", - "description": "Custom client data that will be included in a [Delivery Report](#channels/whatsapp/receive-whatsapp-delivery-reports).", - "maxLength": 4000, - "minLength": 0 - }, - "notifyUrl": { - "type": "string", - "description": "The URL on your callback server to which delivery and seen reports will be sent. [Delivery report format](#channels/whatsapp/receive-whatsapp-delivery-reports), [Seen report format](#channels/whatsapp/receive-whatsapp-seen-reports).", - "maxLength": 2048, - "minLength": 0 - }, - "urlOptions": { - "$ref": "#/components/schemas/f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.UrlOptions" - }, - "entityId": { - "type": "string", - "description": "Required for entity use in a send request for outbound traffic. Returned in notification events. For more details, see our [documentation](https://www.infobip.com/docs/cpaas-x/application-and-entity-management).", - "maxLength": 255, - "minLength": 0 - }, - "applicationId": { - "type": "string", - "description": "Required for application use in a send request for outbound traffic. Returned in notification events. For more details, see our [documentation](https://www.infobip.com/docs/cpaas-x/application-and-entity-management).", - "maxLength": 255, - "minLength": 0 - } - }, - "required": [ - "content", - "from", - "to" - ] - }, - "f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.TextContent": { - "type": "object", - "description": "The content object to build a message that will be sent.", - "properties": { - "text": { - "type": "string", - "description": "Content of the message being sent.", - "maxLength": 4096, - "minLength": 1 - }, - "previewUrl": { - "type": "boolean", - "description": "Allows for URL preview from within the message. If set to `true`, the message content must contain a URL starting with `https://` or `http://`. Defaults to `false`." - } - }, - "required": [ - "text" - ] - }, - "f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.UrlOptions": { - "type": "object", - "description": "Sets up [URL shortening](https://www.infobip.com/docs/url-shortening) and tracking feature.", - "properties": { - "shortenUrl": { - "type": "boolean", - "default": true, - "description": "Enable shortening of the URLs within a message. Set this to `true`, if you want to set up other URL options." - }, - "trackClicks": { - "type": "boolean", - "default": true, - "description": "Enable tracking of short URL clicks within a message: which URL was clicked, how many times, and by whom." - }, - "trackingUrl": { - "type": "string", - "description": "The URL of your callback server on to which the Click report will be sent." - }, - "removeProtocol": { - "type": "boolean", - "default": false, - "description": "Remove a protocol, such as `https://`, from links to shorten a message. Note that some mobiles may not recognize such links as a URL." - }, - "customDomain": { - "type": "string", - "description": "Select a predefined custom domain to use when generating a short URL." - } - } - }, - "f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.SingleMessageInfo": { - "type": "object", - "properties": { - "to": { - "type": "string", - "description": "The destination address of the message.", - "example": "385977666618" - }, - "messageCount": { - "type": "integer", - "format": "int32", - "description": "Number of messages required to deliver.", - "example": 1 - }, - "messageId": { - "type": "string", - "description": "The ID that uniquely identifies the message sent. If not passed, it will be automatically generated and returned in a response.", - "example": "06df139a-7eb5-4a6e-902e-40e892210455" - }, - "status": { - "$ref": "#/components/schemas/f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.SingleMessageStatus" - } - } - }, - "f30d78c250ad1209aef755ddde02bb131e1749b705985f0e9f1b007c900b98e2.SingleMessageStatus": { - "type": "object", - "description": "Indicates the [status](https://www.infobip.com/docs/essentials/response-status-and-error-codes#api-status-codes) of the message and how to recover from an error should there be any.", - "properties": { - "groupId": { - "type": "integer", - "format": "int32", - "description": "Status group ID.", - "example": 1 - }, - "groupName": { - "type": "string", - "description": "Status group name.", - "example": "PENDING" - }, - "id": { - "type": "integer", - "format": "int32", - "description": "Status ID.", - "example": 7 - }, - "name": { - "type": "string", - "description": "Status name.", - "example": "PENDING_ENROUTE" - }, - "description": { - "type": "string", - "description": "Human-readable description of the status.", - "example": "Message sent to next instance" - }, - "action": { - "type": "string", - "description": "Action that should be taken to eliminate the error." - } - } - }, - "a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMedia": { - "type": "object", - "anyOf": [ - { - "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaImage" - }, - { - "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaVideo" - } - ], - "description": "Media information of included referral.", - "properties": { - "type": { - "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.MediaType" - }, - "discriminator": { - "propertyName": "type", - "mapping": { - "IMAGE": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaImage", - "VIDEO": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaVideo" - } - }, - "readOnly": false, - "writeOnly": true - } - }, - "a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaImage": { - "type": "object", - "properties": { - "type": { - "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.MediaType" - }, - "url": { - "type": "string", - "description": "URL that leads to the image that end user saw and clicked.", - "readOnly": false, - "writeOnly": true - } - } - }, - "a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.MediaType": { - "type": "string", - "enum": [ - "IMAGE", - "VIDEO" - ], - "title": "Type" - }, - "a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.ReferralMediaVideo": { - "type": "object", - "properties": { - "type": { - "$ref": "#/components/schemas/a4e0c51a24a360282e1a40c61554d239555404b786d84603912030fc4cdbb512.MediaType" - }, - "url": { - "type": "string", - "description": "URL that leads to the video that end user saw and clicked.", - "readOnly": false, - "writeOnly": true - } - } - } - } - } -} diff --git a/tests/api/openapi/test_whatsapp_api.py b/tests/api/openapi/test_whatsapp_api.py index 2fcefbfe..9d6d92c7 100644 --- a/tests/api/openapi/test_whatsapp_api.py +++ b/tests/api/openapi/test_whatsapp_api.py @@ -1,4 +1,3 @@ -from pathlib import Path from unittest.mock import MagicMock, patch from autogen import UserProxyAgent @@ -66,70 +65,3 @@ def test_real_whatsapp_end2end( "callbackData": "Callback data", }, ) - - -@patch("fastagency.api.openapi.client.requests.post") -def test_real_whatsapp_end2end_problematic( - mock_post: MagicMock, -) -> None: - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.json.return_value = {"status": "success"} - mock_post.return_value = mock_response - - file_name = "whatsapp_simple.json" - - file_path = ( - Path(__file__).parent.parent.parent.parent / f"examples/openapi/{file_name}" - ) - - with file_path.open(encoding="utf-8") as file: - openapi_json = file.read() - - api = OpenAPI.create( - openapi_json=openapi_json, - ) - - assert isinstance(api, OpenAPI) - - user_proxy = UserProxyAgent( - name="user_proxy", - human_input_mode="NEVER", - code_execution_config=False, - ) - - functions = ["send_whatsapp_text_message"] - api._register_for_execution(user_proxy, functions=functions) - - assert tuple(user_proxy._function_map.keys()) == ("send_whatsapp_text_message",) - - send_whatsapp_text_message = user_proxy._function_map["send_whatsapp_text_message"] - - send_whatsapp_text_message( - **{ - "body": { - "from": "447860099299", - "to": "38591152131", - "messageId": "test-message-123", - "content": {"text": "Hello, World!"}, - "callbackData": "Callback data", - } - } - ) - - mock_post.assert_called_once() - - mock_post.assert_called_once_with( - "https://api.infobip.com/whatsapp/1/message/text", - params={}, - headers={ - "Content-Type": "application/json", - }, - json={ - "from": "447860099299", - "to": "38591152131", - "messageId": "test-message-123", - "content": {"text": "Hello, World!"}, - "callbackData": "Callback data", - }, - ) From 8db233a2c7d046c1ad889ec8965454d8c275b8aa Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Mon, 4 Nov 2024 15:01:11 +0000 Subject: [PATCH 16/17] Implement code review sugestions --- docs/docs/en/tutorials/whatsapp/index.md | 16 ++--- docs/docs_src/tutorials/whatsapp/main.py | 1 + fastagency/api/openapi/__init__.py | 4 +- .../openapi/patch_datamodel_code_generator.py | 20 ++----- .../templates/test_fastapi_stale_patch.py | 58 +++++++++++++++++++ 5 files changed, 74 insertions(+), 25 deletions(-) create mode 100644 tests/api/openapi/templates/test_fastapi_stale_patch.py diff --git a/docs/docs/en/tutorials/whatsapp/index.md b/docs/docs/en/tutorials/whatsapp/index.md index 8733e1e1..3fbd4f7f 100644 --- a/docs/docs/en/tutorials/whatsapp/index.md +++ b/docs/docs/en/tutorials/whatsapp/index.md @@ -110,7 +110,7 @@ The following lines shows how to initializes the WhatsApp API by loading the Ope Also, we configure the **WhatsApp API** with the __*WHATSAPP_API_KEY*__ using __*set_security_params*__ to authenticate our requests. ```python -{! docs_src/tutorials/whatsapp/main.py [ln:24-33] !} +{! docs_src/tutorials/whatsapp/main.py [ln:24-34] !} ``` For more information, visit [**API Integration User Guide**](../../user-guide/api/index.md){target="_blank"}. @@ -121,7 +121,7 @@ For more information, visit [**API Integration User Guide**](../../user-guide/ap Here, we initialize a new workflow using ***AutoGenWorkflows()*** and register it under the name ***"whatsapp_and_websurfer"***. The ***@wf.register*** decorator registers the function to handle chat flow with security enabled, combining both WhatsAppAgent and WebSurferAgent. ```python -{! docs_src/tutorials/whatsapp/main.py [ln:60-64] !} +{! docs_src/tutorials/whatsapp/main.py [ln:61-65] !} ... ``` @@ -129,7 +129,7 @@ Here, we initialize a new workflow using ***AutoGenWorkflows()*** and register i This is a core function used by the **WhatsAppAgent** to either present the task result or ask a follow-up question to the user. The message is wrapped in a ***TextInput*** object, and then ***ui.process_message()*** sends it for user interaction. ```python -{! docs_src/tutorials/whatsapp/main.py [ln:68-78] !} +{! docs_src/tutorials/whatsapp/main.py [ln:69-79] !} ``` ### Creating the WhatsApp and WebSurfer Agents @@ -138,7 +138,7 @@ This is a core function used by the **WhatsAppAgent** to either present the task - [**`WebSurferAgent`**](../../api/fastagency/runtimes/autogen/agents/websurfer/WebSurferAgent.md): The [**`WebSurferAgent`**](../../api/fastagency/runtimes/autogen/agents/websurfer/WebSurferAgent.md) is responsible for scraping web content and passes the retrieved data to the **WhatsAppAgent**. It’s configured with a summarizer to condense web content, which is useful when presenting concise data to the user. For more information, visit [**WebSurfer User Guide**](../../user-guide/runtimes/autogen/websurfer.md). ```python -{! docs_src/tutorials/whatsapp/main.py [ln:80-96] !} +{! docs_src/tutorials/whatsapp/main.py [ln:81-97] !} ``` @@ -147,13 +147,13 @@ This is a core function used by the **WhatsAppAgent** to either present the task The function ***present_completed_task_or_ask_question*** is registered to allow the **WhatsAppAgent** to ask questions or present completed tasks after receiving data from the [**`WebSurferAgent`**](../../api/fastagency/runtimes/autogen/agents/websurfer/WebSurferAgent.md). ```python -{! docs_src/tutorials/whatsapp/main.py [ln:98-105] !} +{! docs_src/tutorials/whatsapp/main.py [ln:99-106] !} ``` We register the WhatsApp API, which allows the **WhatsAppAgent** to handle tasks like suggesting messages that will be sent to the user. ```python -{! docs_src/tutorials/whatsapp/main.py [ln:107-112] !} +{! docs_src/tutorials/whatsapp/main.py [ln:108-113] !} ``` ### Initiating the Chat @@ -163,7 +163,7 @@ We initiate the conversation between the user, [**`WebSurferAgent`**](../../api/ Once the conversation ends, the summary is returned to the user, wrapping up the session. ```python -{! docs_src/tutorials/whatsapp/main.py [ln:120-125] !} +{! docs_src/tutorials/whatsapp/main.py [ln:121-126] !} ``` ### Starting the Application @@ -171,7 +171,7 @@ Once the conversation ends, the summary is returned to the user, wrapping up the The FastAgency app is created, using the registered workflows (**`wf`**) and web-based user interface ([**`MesopUI`**](../../api/fastagency/ui/mesop/MesopUI.md)). This makes the conversation between agents and the user interactive. ```python -{! docs_src/tutorials/whatsapp/main.py [ln:130] !} +{! docs_src/tutorials/whatsapp/main.py [ln:131] !} ``` For more information, visit [**Mesop User Guide**](../../user-guide/ui/mesop/basics.md){target="_blank"}. diff --git a/docs/docs_src/tutorials/whatsapp/main.py b/docs/docs_src/tutorials/whatsapp/main.py index a129de44..a189da54 100644 --- a/docs/docs_src/tutorials/whatsapp/main.py +++ b/docs/docs_src/tutorials/whatsapp/main.py @@ -25,6 +25,7 @@ whatsapp_api = OpenAPI.create( openapi_url=openapi_url, + # this is an optional parameter, but specified here because servers are not specified in the OpenAPI specification servers=[{"url": "https://api.infobip.com"}], ) diff --git a/fastagency/api/openapi/__init__.py b/fastagency/api/openapi/__init__.py index cdc96987..281eebbc 100644 --- a/fastagency/api/openapi/__init__.py +++ b/fastagency/api/openapi/__init__.py @@ -2,6 +2,7 @@ check_imports(["fastapi_code_generator", "fastapi", "requests"], "openapi") +from .patch_datamodel_code_generator import patch_apply_discriminator_type # noqa: E402 from .patch_fastapi_code_generator import ( # noqa: E402 patch_function_name_parsing, patch_generate_code, @@ -11,9 +12,6 @@ patch_parse_schema() patch_function_name_parsing() patch_generate_code() - -from .patch_datamodel_code_generator import patch_apply_discriminator_type # noqa: E402 - patch_apply_discriminator_type() from .client import OpenAPI # noqa: E402 diff --git a/fastagency/api/openapi/patch_datamodel_code_generator.py b/fastagency/api/openapi/patch_datamodel_code_generator.py index ed27bdc0..d275346c 100644 --- a/fastagency/api/openapi/patch_datamodel_code_generator.py +++ b/fastagency/api/openapi/patch_datamodel_code_generator.py @@ -19,6 +19,9 @@ logger = get_logger(__name__) +# Save the original method before patching +original_apply_discriminator_type = OpenAPIParser._Parser__apply_discriminator_type + def patch_apply_discriminator_type() -> None: # noqa: C901 def __apply_discriminator_type_patched( # noqa: C901 @@ -135,18 +138,7 @@ def check_paths( if has_imported_literal: # pragma: no cover imports.append(literal) - original_name = [ - name for name in dir(OpenAPIParser) if "__apply_discriminator_type" in name - ] - if original_name: - # Patch the method using the exact mangled name - setattr(OpenAPIParser, original_name[0], __apply_discriminator_type_patched) - else: - # Handle the case if the method does not exist (unexpected) - raise AttributeError( - "Method __apply_discriminator_type not found in base.Parser" - ) + # Patch the method using the exact mangled name + OpenAPIParser._Parser__apply_discriminator_type = __apply_discriminator_type_patched - logger.info( - f"Patched Parser.__apply_discriminator_type, original name: {original_name}" - ) + logger.info("Patched Parser.__apply_discriminator_type,") diff --git a/tests/api/openapi/templates/test_fastapi_stale_patch.py b/tests/api/openapi/templates/test_fastapi_stale_patch.py new file mode 100644 index 00000000..6a5ed8c4 --- /dev/null +++ b/tests/api/openapi/templates/test_fastapi_stale_patch.py @@ -0,0 +1,58 @@ +import tempfile +from pathlib import Path + +import pytest +from datamodel_code_generator import DataModelType +from fastapi_code_generator.__main__ import generate_code +from fastapi_code_generator.parser import OpenAPIParser + +from fastagency.api.openapi.patch_datamodel_code_generator import ( + original_apply_discriminator_type, +) + +OPENAPI_FILE_PATHS = Path(__file__).parent +TEMPLATE_DIR = Path(__file__).parents[4] / "templates" + +assert TEMPLATE_DIR.exists(), TEMPLATE_DIR + + +def test_datamodel_codegen_discriminator_stale_patch(monkeypatch) -> None: + """This test checks if a RuntimeError is raised due to a discriminator containing additional fields in the properties. + + If this error is not raised, it indicates that the original issue has been resolved, + meaning the patch `patch_apply_discriminator_type` applied in + `fastagency/api/openapi/__init__.py` is likely stale and can be removed. + """ + monkeypatch.setattr( + OpenAPIParser, + "_Parser__apply_discriminator_type", + original_apply_discriminator_type, + ) + + discriminator_with_properties_spec_path = ( + OPENAPI_FILE_PATHS / "discriminator_in_root_with_properties.json" + ) + + with tempfile.TemporaryDirectory() as temp_dir: + td = Path(temp_dir) + + # Expecting RuntimeError if patch is still required; if not, the patch may be obsolete. + with pytest.raises( + RuntimeError, + match="Discriminator type is not found.", + ): + generate_code( + input_name=discriminator_with_properties_spec_path.name, + input_text=discriminator_with_properties_spec_path.read_text(), + encoding="utf-8", + output_dir=td, + template_dir=TEMPLATE_DIR, + output_model_type=DataModelType.PydanticV2BaseModel, + custom_visitors=[ + Path(__file__).parents[4] + / "fastagency" + / "api" + / "openapi" + / "security_schema_visitor.py" + ], + ) From 3c362c18d453dd7bbadee311022374f9e262b967 Mon Sep 17 00:00:00 2001 From: Tvrtko Sternak Date: Mon, 4 Nov 2024 15:08:41 +0000 Subject: [PATCH 17/17] Fix typing --- tests/api/openapi/templates/test_fastapi_stale_patch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/api/openapi/templates/test_fastapi_stale_patch.py b/tests/api/openapi/templates/test_fastapi_stale_patch.py index 6a5ed8c4..93808ba2 100644 --- a/tests/api/openapi/templates/test_fastapi_stale_patch.py +++ b/tests/api/openapi/templates/test_fastapi_stale_patch.py @@ -16,7 +16,9 @@ assert TEMPLATE_DIR.exists(), TEMPLATE_DIR -def test_datamodel_codegen_discriminator_stale_patch(monkeypatch) -> None: +def test_datamodel_codegen_discriminator_stale_patch( + monkeypatch: pytest.MonkeyPatch, +) -> None: """This test checks if a RuntimeError is raised due to a discriminator containing additional fields in the properties. If this error is not raised, it indicates that the original issue has been resolved,