From b60c125c557798c7aaa16ab72e35a85347dd3bf1 Mon Sep 17 00:00:00 2001 From: Ashwin Bharambe Date: Tue, 3 Sep 2024 21:22:30 -0700 Subject: [PATCH] Add pyopenapi fork to the repository, update RFC assets --- .../llama-stack-spec.html | 689 ++++++++++------- .../llama-stack-spec.yaml | 351 +++++---- rfcs/openapi_generator/generate.py | 48 +- rfcs/openapi_generator/pyopenapi/README.md | 1 + rfcs/openapi_generator/pyopenapi/__init__.py | 5 + rfcs/openapi_generator/pyopenapi/generator.py | 718 ++++++++++++++++++ .../openapi_generator/pyopenapi/operations.py | 386 ++++++++++ rfcs/openapi_generator/pyopenapi/options.py | 75 ++ .../pyopenapi/specification.py | 258 +++++++ .../openapi_generator/pyopenapi/template.html | 41 + rfcs/openapi_generator/pyopenapi/utility.py | 116 +++ .../run_openapi_generator.sh | 4 +- 12 files changed, 2240 insertions(+), 452 deletions(-) create mode 100644 rfcs/openapi_generator/pyopenapi/README.md create mode 100644 rfcs/openapi_generator/pyopenapi/__init__.py create mode 100644 rfcs/openapi_generator/pyopenapi/generator.py create mode 100644 rfcs/openapi_generator/pyopenapi/operations.py create mode 100644 rfcs/openapi_generator/pyopenapi/options.py create mode 100644 rfcs/openapi_generator/pyopenapi/specification.py create mode 100644 rfcs/openapi_generator/pyopenapi/template.html create mode 100644 rfcs/openapi_generator/pyopenapi/utility.py diff --git a/rfcs/RFC-0001-llama-stack-assets/llama-stack-spec.html b/rfcs/RFC-0001-llama-stack-assets/llama-stack-spec.html index aabf8aa84..1f65b7873 100644 --- a/rfcs/RFC-0001-llama-stack-assets/llama-stack-spec.html +++ b/rfcs/RFC-0001-llama-stack-assets/llama-stack-spec.html @@ -21,7 +21,7 @@ "info": { "title": "[DRAFT] Llama Stack Specification", "version": "0.0.1", - "description": "This is the specification of the llama stack that provides\n a set of endpoints and their corresponding interfaces that are tailored to\n best leverage Llama Models. The specification is still in draft and subject to change.\n Generated at 2024-08-23 06:36:10.417114" + "description": "This is the specification of the llama stack that provides\n a set of endpoints and their corresponding interfaces that are tailored to\n best leverage Llama Models. The specification is still in draft and subject to change.\n Generated at 2024-09-03 21:36:00.770405" }, "servers": [ { @@ -29,7 +29,7 @@ } ], "paths": { - "/inference/batch_chat_completion": { + "/batch_inference/chat_completion": { "post": { "responses": { "200": { @@ -44,7 +44,7 @@ } }, "tags": [ - "Inference" + "BatchInference" ], "parameters": [], "requestBody": { @@ -59,7 +59,7 @@ } } }, - "/inference/batch_completion": { + "/batch_inference/completion": { "post": { "responses": { "200": { @@ -74,7 +74,7 @@ } }, "tags": [ - "Inference" + "BatchInference" ], "parameters": [], "requestBody": { @@ -550,6 +550,58 @@ ] } }, + "/inference/embeddings": { + "post": { + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmbeddingsResponse" + } + } + } + } + }, + "tags": [ + "Inference" + ], + "parameters": [ + { + "name": "model", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + } + } + }, + "required": true + } + } + }, "/evaluate/question_answering/": { "post": { "responses": { @@ -1053,7 +1105,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MemoryBank" + "oneOf": [ + { + "$ref": "#/components/schemas/MemoryBank" + }, + { + "type": "null" + } + ] } } } @@ -1228,6 +1287,14 @@ "schema": { "type": "string" } + }, + { + "name": "ttl_seconds", + "in": "query", + "required": false, + "schema": { + "type": "integer" + } } ], "requestBody": { @@ -1973,8 +2040,8 @@ "json", "function_tag" ], - "title": "This Enum refers to the prompt format for calling zero shot tools", - "description": "`json` --\n Refers to the json format for calling tools.\n The json format takes the form like\n {\n \"type\": \"function\",\n \"function\" : {\n \"name\": \"function_name\",\n \"description\": \"function_description\",\n \"parameters\": {...}\n }\n }\n\n`function_tag` --\n This is an example of how you could define\n your own user defined format for making tool calls.\n The function_tag format looks like this,\n (parameters)\n\nThe detailed prompts for each of these formats are defined in `system_prompt.py`" + "title": "This Enum refers to the prompt format for calling custom / zero shot tools", + "description": "`json` --\n Refers to the json format for calling tools.\n The json format takes the form like\n {\n \"type\": \"function\",\n \"function\" : {\n \"name\": \"function_name\",\n \"description\": \"function_description\",\n \"parameters\": {...}\n }\n }\n\n`function_tag` --\n This is an example of how you could define\n your own user defined format for making tool calls.\n The function_tag format looks like this,\n (parameters)\n\nThe detailed prompts for each of these formats are added to llama cli" }, "ToolResponseMessage": { "type": "object", @@ -2037,6 +2104,19 @@ } } ] + }, + "context": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] } }, "additionalProperties": false, @@ -2393,95 +2473,6 @@ }, "instructions": { "type": "string" - }, - "memory_bank_configs": { - "type": "array", - "items": { - "oneOf": [ - { - "type": "object", - "properties": { - "bank_id": { - "type": "string" - }, - "type": { - "type": "string", - "const": "vector" - } - }, - "additionalProperties": false, - "required": [ - "bank_id", - "type" - ] - }, - { - "type": "object", - "properties": { - "bank_id": { - "type": "string" - }, - "type": { - "type": "string", - "const": "keyvalue" - }, - "keys": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false, - "required": [ - "bank_id", - "type", - "keys" - ] - }, - { - "type": "object", - "properties": { - "bank_id": { - "type": "string" - }, - "type": { - "type": "string", - "const": "keyword" - } - }, - "additionalProperties": false, - "required": [ - "bank_id", - "type" - ] - }, - { - "type": "object", - "properties": { - "bank_id": { - "type": "string" - }, - "type": { - "type": "string", - "const": "graph" - }, - "entities": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false, - "required": [ - "bank_id", - "type", - "entities" - ] - } - ] - } } }, "additionalProperties": false, @@ -2579,6 +2570,9 @@ "type": "string", "const": "function_call" }, + "function_name": { + "type": "string" + }, "description": { "type": "string" }, @@ -2595,90 +2589,11 @@ "additionalProperties": false, "required": [ "type", + "function_name", "description", "parameters" ] }, - "MemoryBank": { - "type": "object", - "properties": { - "bank_id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "config": { - "oneOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "vector" - }, - "embedding_model": { - "type": "string" - } - }, - "additionalProperties": false, - "required": [ - "type", - "embedding_model" - ] - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "keyvalue" - } - }, - "additionalProperties": false, - "required": [ - "type" - ] - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "keyword" - } - }, - "additionalProperties": false, - "required": [ - "type" - ] - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "graph" - } - }, - "additionalProperties": false, - "required": [ - "type" - ] - } - ] - }, - "url": { - "$ref": "#/components/schemas/URL" - } - }, - "additionalProperties": false, - "required": [ - "bank_id", - "name", - "config" - ] - }, "MemoryToolDefinition": { "type": "object", "properties": { @@ -2698,17 +2613,108 @@ "type": "string", "const": "memory" }, - "memory_banks": { + "memory_bank_configs": { "type": "array", "items": { - "$ref": "#/components/schemas/MemoryBank" + "oneOf": [ + { + "type": "object", + "properties": { + "bank_id": { + "type": "string" + }, + "type": { + "type": "string", + "const": "vector" + } + }, + "additionalProperties": false, + "required": [ + "bank_id", + "type" + ] + }, + { + "type": "object", + "properties": { + "bank_id": { + "type": "string" + }, + "type": { + "type": "string", + "const": "keyvalue" + }, + "keys": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": [ + "bank_id", + "type", + "keys" + ] + }, + { + "type": "object", + "properties": { + "bank_id": { + "type": "string" + }, + "type": { + "type": "string", + "const": "keyword" + } + }, + "additionalProperties": false, + "required": [ + "bank_id", + "type" + ] + }, + { + "type": "object", + "properties": { + "bank_id": { + "type": "string" + }, + "type": { + "type": "string", + "const": "graph" + }, + "entities": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "required": [ + "bank_id", + "type", + "entities" + ] + } + ] } + }, + "max_tokens_in_context": { + "type": "integer" + }, + "max_chunks": { + "type": "integer" } }, "additionalProperties": false, "required": [ "type", - "memory_banks" + "memory_bank_configs", + "max_tokens_in_context", + "max_chunks" ] }, "OnViolationAction": { @@ -2973,8 +2979,21 @@ "Attachment": { "type": "object", "properties": { - "url": { - "$ref": "#/components/schemas/URL" + "content": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "$ref": "#/components/schemas/URL" + } + ] }, "mime_type": { "type": "string" @@ -2982,7 +3001,7 @@ }, "additionalProperties": false, "required": [ - "url", + "content", "mime_type" ] }, @@ -3177,12 +3196,19 @@ }, "embedding_model": { "type": "string" + }, + "chunk_size_in_tokens": { + "type": "integer" + }, + "overlap_size_in_tokens": { + "type": "integer" } }, "additionalProperties": false, "required": [ "type", - "embedding_model" + "embedding_model", + "chunk_size_in_tokens" ] }, { @@ -3235,6 +3261,93 @@ "config" ] }, + "MemoryBank": { + "type": "object", + "properties": { + "bank_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "config": { + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "vector" + }, + "embedding_model": { + "type": "string" + }, + "chunk_size_in_tokens": { + "type": "integer" + }, + "overlap_size_in_tokens": { + "type": "integer" + } + }, + "additionalProperties": false, + "required": [ + "type", + "embedding_model", + "chunk_size_in_tokens" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "keyvalue" + } + }, + "additionalProperties": false, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "keyword" + } + }, + "additionalProperties": false, + "required": [ + "type" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "graph" + } + }, + "additionalProperties": false, + "required": [ + "type" + ] + } + ] + }, + "url": { + "$ref": "#/components/schemas/URL" + } + }, + "additionalProperties": false, + "required": [ + "bank_id", + "name", + "config" + ] + }, "CreateRunRequest": { "type": "object", "properties": { @@ -3327,6 +3440,24 @@ "metadata" ] }, + "EmbeddingsResponse": { + "type": "object", + "properties": { + "embeddings": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "number" + } + } + } + }, + "additionalProperties": false, + "required": [ + "embeddings" + ] + }, "Checkpoint": { "description": "Checkpoint created during training runs" }, @@ -3484,65 +3615,6 @@ "model_response" ] }, - "MemoryBankDocument": { - "type": "object", - "properties": { - "document_id": { - "type": "string" - }, - "content": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "$ref": "#/components/schemas/URL" - } - ] - }, - "mime_type": { - "type": "string" - }, - "metadata": { - "type": "object", - "additionalProperties": { - "oneOf": [ - { - "type": "null" - }, - { - "type": "boolean" - }, - { - "type": "number" - }, - { - "type": "string" - }, - { - "type": "array" - }, - { - "type": "object" - } - ] - } - } - }, - "additionalProperties": false, - "required": [ - "document_id", - "content", - "mime_type", - "metadata" - ] - }, "MemoryRetrievalStep": { "type": "object", "properties": { @@ -3570,17 +3642,18 @@ "type": "string" } }, - "documents": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MemoryBankDocument" - } - }, - "scores": { - "type": "array", - "items": { - "type": "number" - } + "inserted_context": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] } }, "additionalProperties": false, @@ -3589,8 +3662,7 @@ "step_id", "step_type", "memory_bank_ids", - "documents", - "scores" + "inserted_context" ] }, "Session": { @@ -3611,6 +3683,9 @@ "started_at": { "type": "string", "format": "date-time" + }, + "memory_bank": { + "$ref": "#/components/schemas/MemoryBank" } }, "additionalProperties": false, @@ -3928,6 +4003,65 @@ "other" ] }, + "MemoryBankDocument": { + "type": "object", + "properties": { + "document_id": { + "type": "string" + }, + "content": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "$ref": "#/components/schemas/URL" + } + ] + }, + "mime_type": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "boolean" + }, + { + "type": "number" + }, + { + "type": "string" + }, + { + "type": "array" + }, + { + "type": "object" + } + ] + } + } + }, + "additionalProperties": false, + "required": [ + "document_id", + "content", + "mime_type", + "metadata" + ] + }, "EvaluationJobArtifactsResponse": { "type": "object", "properties": { @@ -4504,12 +4638,16 @@ }, "token_count": { "type": "integer" + }, + "document_id": { + "type": "string" } }, "additionalProperties": false, "required": [ "content", - "token_count" + "token_count", + "document_id" ] } }, @@ -5109,7 +5247,7 @@ ], "tags": [ { - "name": "Observability" + "name": "BatchInference" }, { "name": "AgenticSystem" @@ -5121,19 +5259,22 @@ "name": "Memory" }, { - "name": "Evaluations" + "name": "Observability" }, { - "name": "Datasets" + "name": "SyntheticDataGeneration" + }, + { + "name": "Evaluations" }, { "name": "RewardScoring" }, { - "name": "Inference" + "name": "Datasets" }, { - "name": "SyntheticDataGeneration" + "name": "Inference" }, { "name": "BatchChatCompletionRequest", @@ -5181,7 +5322,7 @@ }, { "name": "ToolPromptFormat", - "description": "This Enum refers to the prompt format for calling zero shot tools\n\n`json` --\n Refers to the json format for calling tools.\n The json format takes the form like\n {\n \"type\": \"function\",\n \"function\" : {\n \"name\": \"function_name\",\n \"description\": \"function_description\",\n \"parameters\": {...}\n }\n }\n\n`function_tag` --\n This is an example of how you could define\n your own user defined format for making tool calls.\n The function_tag format looks like this,\n (parameters)\n\nThe detailed prompts for each of these formats are defined in `system_prompt.py`\n\n" + "description": "This Enum refers to the prompt format for calling custom / zero shot tools\n\n`json` --\n Refers to the json format for calling tools.\n The json format takes the form like\n {\n \"type\": \"function\",\n \"function\" : {\n \"name\": \"function_name\",\n \"description\": \"function_description\",\n \"parameters\": {...}\n }\n }\n\n`function_tag` --\n This is an example of how you could define\n your own user defined format for making tool calls.\n The function_tag format looks like this,\n (parameters)\n\nThe detailed prompts for each of these formats are added to llama cli\n\n" }, { "name": "ToolResponseMessage", @@ -5259,10 +5400,6 @@ "name": "FunctionCallToolDefinition", "description": "" }, - { - "name": "MemoryBank", - "description": "" - }, { "name": "MemoryToolDefinition", "description": "" @@ -5343,6 +5480,10 @@ "name": "CreateMemoryBankRequest", "description": "" }, + { + "name": "MemoryBank", + "description": "" + }, { "name": "CreateRunRequest", "description": "" @@ -5351,6 +5492,10 @@ "name": "Run", "description": "" }, + { + "name": "EmbeddingsResponse", + "description": "" + }, { "name": "Checkpoint", "description": "Checkpoint created during training runs\n\n" @@ -5375,10 +5520,6 @@ "name": "InferenceStep", "description": "" }, - { - "name": "MemoryBankDocument", - "description": "" - }, { "name": "MemoryRetrievalStep", "description": "" @@ -5419,6 +5560,10 @@ "name": "ArtifactType", "description": "" }, + { + "name": "MemoryBankDocument", + "description": "" + }, { "name": "EvaluationJobArtifactsResponse", "description": "Artifacts of a evaluation job.\n\n" @@ -5565,6 +5710,7 @@ "name": "Operations", "tags": [ "AgenticSystem", + "BatchInference", "Datasets", "Evaluations", "Inference", @@ -5610,6 +5756,7 @@ "DPOAlignmentConfig", "DialogGenerations", "DoraFinetuningConfig", + "EmbeddingsResponse", "EvaluateQuestionAnsweringRequest", "EvaluateSummarizationRequest", "EvaluateTextGenerationRequest", diff --git a/rfcs/RFC-0001-llama-stack-assets/llama-stack-spec.yaml b/rfcs/RFC-0001-llama-stack-assets/llama-stack-spec.yaml index 019790a62..9e7a2268d 100644 --- a/rfcs/RFC-0001-llama-stack-assets/llama-stack-spec.yaml +++ b/rfcs/RFC-0001-llama-stack-assets/llama-stack-spec.yaml @@ -10,64 +10,6 @@ components: type: array instructions: type: string - memory_bank_configs: - items: - oneOf: - - additionalProperties: false - properties: - bank_id: - type: string - type: - const: vector - type: string - required: - - bank_id - - type - type: object - - additionalProperties: false - properties: - bank_id: - type: string - keys: - items: - type: string - type: array - type: - const: keyvalue - type: string - required: - - bank_id - - type - - keys - type: object - - additionalProperties: false - properties: - bank_id: - type: string - type: - const: keyword - type: string - required: - - bank_id - - type - type: object - - additionalProperties: false - properties: - bank_id: - type: string - entities: - items: - type: string - type: array - type: - const: graph - type: string - required: - - bank_id - - type - - entities - type: object - type: array model: type: string output_shields: @@ -220,12 +162,17 @@ components: Attachment: additionalProperties: false properties: + content: + oneOf: + - type: string + - items: + type: string + type: array + - $ref: '#/components/schemas/URL' mime_type: type: string - url: - $ref: '#/components/schemas/URL' required: - - url + - content - mime_type type: object BatchChatCompletionRequest: @@ -537,14 +484,19 @@ components: oneOf: - additionalProperties: false properties: + chunk_size_in_tokens: + type: integer embedding_model: type: string + overlap_size_in_tokens: + type: integer type: const: vector type: string required: - type - embedding_model + - chunk_size_in_tokens type: object - additionalProperties: false properties: @@ -655,6 +607,18 @@ components: - rank - alpha type: object + EmbeddingsResponse: + additionalProperties: false + properties: + embeddings: + items: + items: + type: number + type: array + type: array + required: + - embeddings + type: object EvaluateQuestionAnsweringRequest: additionalProperties: false properties: @@ -819,6 +783,8 @@ components: properties: description: type: string + function_name: + type: string input_shields: items: $ref: '#/components/schemas/ShieldDefinition' @@ -838,6 +804,7 @@ components: type: string required: - type + - function_name - description - parameters type: object @@ -965,14 +932,19 @@ components: oneOf: - additionalProperties: false properties: + chunk_size_in_tokens: + type: integer embedding_model: type: string + overlap_size_in_tokens: + type: integer type: const: vector type: string required: - type - embedding_model + - chunk_size_in_tokens type: object - additionalProperties: false properties: @@ -1043,18 +1015,16 @@ components: completed_at: format: date-time type: string - documents: - items: - $ref: '#/components/schemas/MemoryBankDocument' - type: array + inserted_context: + oneOf: + - type: string + - items: + type: string + type: array memory_bank_ids: items: type: string type: array - scores: - items: - type: number - type: array started_at: format: date-time type: string @@ -1070,8 +1040,7 @@ components: - step_id - step_type - memory_bank_ids - - documents - - scores + - inserted_context type: object MemoryToolDefinition: additionalProperties: false @@ -1080,9 +1049,67 @@ components: items: $ref: '#/components/schemas/ShieldDefinition' type: array - memory_banks: + max_chunks: + type: integer + max_tokens_in_context: + type: integer + memory_bank_configs: items: - $ref: '#/components/schemas/MemoryBank' + oneOf: + - additionalProperties: false + properties: + bank_id: + type: string + type: + const: vector + type: string + required: + - bank_id + - type + type: object + - additionalProperties: false + properties: + bank_id: + type: string + keys: + items: + type: string + type: array + type: + const: keyvalue + type: string + required: + - bank_id + - type + - keys + type: object + - additionalProperties: false + properties: + bank_id: + type: string + type: + const: keyword + type: string + required: + - bank_id + - type + type: object + - additionalProperties: false + properties: + bank_id: + type: string + entities: + items: + type: string + type: array + type: + const: graph + type: string + required: + - bank_id + - type + - entities + type: object type: array output_shields: items: @@ -1093,7 +1120,9 @@ components: type: string required: - type - - memory_banks + - memory_bank_configs + - max_tokens_in_context + - max_chunks type: object Metric: additionalProperties: false @@ -1406,11 +1435,14 @@ components: - items: type: string type: array + document_id: + type: string token_count: type: integer required: - content - token_count + - document_id type: object type: array scores: @@ -1575,6 +1607,8 @@ components: Session: additionalProperties: false properties: + memory_bank: + $ref: '#/components/schemas/MemoryBank' session_id: type: string session_name: @@ -1869,11 +1903,12 @@ components: : {...}\n }\n }\n\n`function_tag` --\n This is an example of\ \ how you could define\n your own user defined format for making tool calls.\n\ \ The function_tag format looks like this,\n (parameters)\n\ - \nThe detailed prompts for each of these formats are defined in `system_prompt.py`" + \nThe detailed prompts for each of these formats are added to llama cli" enum: - json - function_tag - title: This Enum refers to the prompt format for calling zero shot tools + title: This Enum refers to the prompt format for calling custom / zero shot + tools type: string ToolResponse: additionalProperties: false @@ -2104,6 +2139,12 @@ components: - items: type: string type: array + context: + oneOf: + - type: string + - items: + type: string + type: array role: const: user type: string @@ -2134,7 +2175,7 @@ info: description: "This is the specification of the llama stack that provides\n \ \ a set of endpoints and their corresponding interfaces that are tailored\ \ to\n best leverage Llama Models. The specification is still in\ - \ draft and subject to change.\n Generated at 2024-08-23 06:36:10.417114" + \ draft and subject to change.\n Generated at 2024-09-03 21:36:00.770405" title: '[DRAFT] Llama Stack Specification' version: 0.0.1 jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema @@ -2327,6 +2368,42 @@ paths: description: OK tags: - Observability + /batch_inference/chat_completion: + post: + parameters: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/BatchChatCompletionRequest' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/BatchChatCompletionResponse' + description: OK + tags: + - BatchInference + /batch_inference/completion: + post: + parameters: [] + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/BatchCompletionRequest' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/BatchCompletionResponse' + description: OK + tags: + - BatchInference /datasets/create: post: parameters: [] @@ -2619,42 +2696,6 @@ paths: description: OK tags: - Observability - /inference/batch_chat_completion: - post: - parameters: [] - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/BatchChatCompletionRequest' - required: true - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BatchChatCompletionResponse' - description: OK - tags: - - Inference - /inference/batch_completion: - post: - parameters: [] - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/BatchCompletionRequest' - required: true - responses: - '200': - content: - application/json: - schema: - $ref: '#/components/schemas/BatchCompletionResponse' - description: OK - tags: - - Inference /inference/chat_completion: post: parameters: [] @@ -2691,6 +2732,35 @@ paths: description: streamed completion response. tags: - Inference + /inference/embeddings: + post: + parameters: + - in: query + name: model + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + items: + oneOf: + - type: string + - items: + type: string + type: array + type: array + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/EmbeddingsResponse' + description: OK + tags: + - Inference /logging/get_logs: post: parameters: [] @@ -2777,6 +2847,11 @@ paths: required: true schema: type: string + - in: query + name: ttl_seconds + required: false + schema: + type: integer requestBody: content: application/json: @@ -2887,7 +2962,9 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/MemoryBank' + oneOf: + - $ref: '#/components/schemas/MemoryBank' + - type: 'null' description: OK tags: - Memory @@ -3105,15 +3182,16 @@ security: servers: - url: http://any-hosted-llama-stack.com tags: -- name: Observability +- name: BatchInference - name: AgenticSystem - name: PostTraining - name: Memory -- name: Evaluations -- name: Datasets -- name: RewardScoring -- name: Inference +- name: Observability - name: SyntheticDataGeneration +- name: Evaluations +- name: RewardScoring +- name: Datasets +- name: Inference - description: name: BatchChatCompletionRequest @@ -3140,16 +3218,16 @@ tags: - description: name: ToolParamDefinition -- description: "This Enum refers to the prompt format for calling zero shot tools\n\ - \n`json` --\n Refers to the json format for calling tools.\n The json format\ - \ takes the form like\n {\n \"type\": \"function\",\n \"function\"\ - \ : {\n \"name\": \"function_name\",\n \"description\":\ - \ \"function_description\",\n \"parameters\": {...}\n }\n \ - \ }\n\n`function_tag` --\n This is an example of how you could define\n \ - \ your own user defined format for making tool calls.\n The function_tag format\ - \ looks like this,\n (parameters)\n\nThe\ - \ detailed prompts for each of these formats are defined in `system_prompt.py`\n\ - \n" +- description: "This Enum refers to the prompt format for calling custom / zero shot\ + \ tools\n\n`json` --\n Refers to the json format for calling tools.\n The\ + \ json format takes the form like\n {\n \"type\": \"function\",\n \ + \ \"function\" : {\n \"name\": \"function_name\",\n \ + \ \"description\": \"function_description\",\n \"parameters\": {...}\n\ + \ }\n }\n\n`function_tag` --\n This is an example of how you could\ + \ define\n your own user defined format for making tool calls.\n The function_tag\ + \ format looks like this,\n (parameters)\n\ + \nThe detailed prompts for each of these formats are added to llama cli\n\n" name: ToolPromptFormat - description: @@ -3212,8 +3290,6 @@ tags: - description: name: FunctionCallToolDefinition -- description: - name: MemoryBank - description: name: MemoryToolDefinition @@ -3277,11 +3353,16 @@ tags: - description: name: CreateMemoryBankRequest +- description: + name: MemoryBank - description: name: CreateRunRequest - description: name: Run +- description: + name: EmbeddingsResponse - description: 'Checkpoint created during training runs @@ -3309,9 +3390,6 @@ tags: name: EvaluateTextGenerationRequest - description: name: InferenceStep -- description: - name: MemoryBankDocument - description: name: MemoryRetrievalStep @@ -3341,6 +3419,9 @@ tags: name: Artifact - description: name: ArtifactType +- description: + name: MemoryBankDocument - description: 'Artifacts of a evaluation job. @@ -3474,6 +3555,7 @@ x-tagGroups: - name: Operations tags: - AgenticSystem + - BatchInference - Datasets - Evaluations - Inference @@ -3516,6 +3598,7 @@ x-tagGroups: - DPOAlignmentConfig - DialogGenerations - DoraFinetuningConfig + - EmbeddingsResponse - EvaluateQuestionAnsweringRequest - EvaluateSummarizationRequest - EvaluateTextGenerationRequest diff --git a/rfcs/openapi_generator/generate.py b/rfcs/openapi_generator/generate.py index 64f2c8465..a269a4bfc 100644 --- a/rfcs/openapi_generator/generate.py +++ b/rfcs/openapi_generator/generate.py @@ -10,17 +10,13 @@ # This source code is licensed under the terms described found in the # LICENSE file in the root directory of this source tree. -import inspect - from datetime import datetime from pathlib import Path -from typing import Callable, Iterator, List, Tuple import fire import yaml from llama_models import schema_utils -from pyopenapi import Info, operations, Options, Server, Specification # We do a series of monkey-patching to ensure our definitions only use the minimal # (json_schema_type, webmethod) definitions from the llama_models package. For @@ -28,7 +24,10 @@ from pyopenapi import Info, operations, Options, Server, Specification # (python-openapi, json-strong-typing) packages. from strong_typing.schema import json_schema_type -from termcolor import colored + +from .pyopenapi.options import Options +from .pyopenapi.specification import Info, Server +from .pyopenapi.utility import Specification schema_utils.json_schema_type = json_schema_type @@ -36,45 +35,6 @@ schema_utils.json_schema_type = json_schema_type from llama_toolchain.stack import LlamaStack -def patched_get_endpoint_functions( - endpoint: type, prefixes: List[str] -) -> Iterator[Tuple[str, str, str, Callable]]: - if not inspect.isclass(endpoint): - raise ValueError(f"object is not a class type: {endpoint}") - - functions = inspect.getmembers(endpoint, inspect.isfunction) - for func_name, func_ref in functions: - webmethod = getattr(func_ref, "__webmethod__", None) - if not webmethod: - continue - - print(f"Processing {colored(func_name, 'white')}...") - operation_name = func_name - if operation_name.startswith("get_") or operation_name.endswith("/get"): - prefix = "get" - elif ( - operation_name.startswith("delete_") - or operation_name.startswith("remove_") - or operation_name.endswith("/delete") - or operation_name.endswith("/remove") - ): - prefix = "delete" - else: - if webmethod.method == "GET": - prefix = "get" - elif webmethod.method == "DELETE": - prefix = "delete" - else: - # by default everything else is a POST - prefix = "post" - - yield prefix, operation_name, func_name, func_ref - - -# Patch this so all methods are correctly parsed with correct HTTP methods -operations._get_endpoint_functions = patched_get_endpoint_functions - - def main(output_dir: str): output_dir = Path(output_dir) if not output_dir.exists(): diff --git a/rfcs/openapi_generator/pyopenapi/README.md b/rfcs/openapi_generator/pyopenapi/README.md new file mode 100644 index 000000000..1b5fbce19 --- /dev/null +++ b/rfcs/openapi_generator/pyopenapi/README.md @@ -0,0 +1 @@ +This is forked from https://github.com/hunyadi/pyopenapi diff --git a/rfcs/openapi_generator/pyopenapi/__init__.py b/rfcs/openapi_generator/pyopenapi/__init__.py new file mode 100644 index 000000000..756f351d8 --- /dev/null +++ b/rfcs/openapi_generator/pyopenapi/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the terms described in the LICENSE file in +# the root directory of this source tree. diff --git a/rfcs/openapi_generator/pyopenapi/generator.py b/rfcs/openapi_generator/pyopenapi/generator.py new file mode 100644 index 000000000..576746e11 --- /dev/null +++ b/rfcs/openapi_generator/pyopenapi/generator.py @@ -0,0 +1,718 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the terms described in the LICENSE file in +# the root directory of this source tree. + +import hashlib +import ipaddress +import typing +from typing import Any, Dict, Set, Union + +from strong_typing.core import JsonType +from strong_typing.docstring import Docstring, parse_type +from strong_typing.inspection import ( + is_generic_list, + is_type_optional, + is_type_union, + unwrap_generic_list, + unwrap_optional_type, + unwrap_union_types, +) +from strong_typing.name import python_type_to_name +from strong_typing.schema import ( + get_schema_identifier, + JsonSchemaGenerator, + register_schema, + Schema, + SchemaOptions, +) +from strong_typing.serialization import json_dump_string, object_to_json + +from .operations import ( + EndpointOperation, + get_endpoint_events, + get_endpoint_operations, + HTTPMethod, +) +from .options import * +from .specification import ( + Components, + Document, + Example, + ExampleRef, + MediaType, + Operation, + Parameter, + ParameterLocation, + PathItem, + RequestBody, + Response, + ResponseRef, + SchemaOrRef, + SchemaRef, + Tag, + TagGroup, +) + +register_schema( + ipaddress.IPv4Address, + schema={ + "type": "string", + "format": "ipv4", + "title": "IPv4 address", + "description": "IPv4 address, according to dotted-quad ABNF syntax as defined in RFC 2673, section 3.2.", + }, + examples=["192.0.2.0", "198.51.100.1", "203.0.113.255"], +) + +register_schema( + ipaddress.IPv6Address, + schema={ + "type": "string", + "format": "ipv6", + "title": "IPv6 address", + "description": "IPv6 address, as defined in RFC 2373, section 2.2.", + }, + examples=[ + "FEDC:BA98:7654:3210:FEDC:BA98:7654:3210", + "1080:0:0:0:8:800:200C:417A", + "1080::8:800:200C:417A", + "FF01::101", + "::1", + ], +) + + +def http_status_to_string(status_code: HTTPStatusCode) -> str: + "Converts an HTTP status code to a string." + + if isinstance(status_code, HTTPStatus): + return str(status_code.value) + elif isinstance(status_code, int): + return str(status_code) + elif isinstance(status_code, str): + return status_code + else: + raise TypeError("expected: HTTP status code") + + +class SchemaBuilder: + schema_generator: JsonSchemaGenerator + schemas: Dict[str, Schema] + + def __init__(self, schema_generator: JsonSchemaGenerator) -> None: + self.schema_generator = schema_generator + self.schemas = {} + + def classdef_to_schema(self, typ: type) -> Schema: + """ + Converts a type to a JSON schema. + For nested types found in the type hierarchy, adds the type to the schema registry in the OpenAPI specification section `components`. + """ + + type_schema, type_definitions = self.schema_generator.classdef_to_schema(typ) + + # append schema to list of known schemas, to be used in OpenAPI's Components Object section + for ref, schema in type_definitions.items(): + self._add_ref(ref, schema) + + return type_schema + + def classdef_to_named_schema(self, name: str, typ: type) -> Schema: + schema = self.classdef_to_schema(typ) + self._add_ref(name, schema) + return schema + + def classdef_to_ref(self, typ: type) -> SchemaOrRef: + """ + Converts a type to a JSON schema, and if possible, returns a schema reference. + For composite types (such as classes), adds the type to the schema registry in the OpenAPI specification section `components`. + """ + + type_schema = self.classdef_to_schema(typ) + if typ is str or typ is int or typ is float: + # represent simple types as themselves + return type_schema + + type_name = get_schema_identifier(typ) + if type_name is not None: + return self._build_ref(type_name, type_schema) + + try: + type_name = python_type_to_name(typ) + return self._build_ref(type_name, type_schema) + except TypeError: + pass + + return type_schema + + def _build_ref(self, type_name: str, type_schema: Schema) -> SchemaRef: + self._add_ref(type_name, type_schema) + return SchemaRef(type_name) + + def _add_ref(self, type_name: str, type_schema: Schema) -> None: + if type_name not in self.schemas: + self.schemas[type_name] = type_schema + + +class ContentBuilder: + schema_builder: SchemaBuilder + schema_transformer: Optional[Callable[[SchemaOrRef], SchemaOrRef]] + sample_transformer: Optional[Callable[[JsonType], JsonType]] + + def __init__( + self, + schema_builder: SchemaBuilder, + schema_transformer: Optional[Callable[[SchemaOrRef], SchemaOrRef]] = None, + sample_transformer: Optional[Callable[[JsonType], JsonType]] = None, + ) -> None: + self.schema_builder = schema_builder + self.schema_transformer = schema_transformer + self.sample_transformer = sample_transformer + + def build_content( + self, payload_type: type, examples: Optional[List[Any]] = None + ) -> Dict[str, MediaType]: + "Creates the content subtree for a request or response." + + if is_generic_list(payload_type): + media_type = "application/jsonl" + item_type = unwrap_generic_list(payload_type) + else: + media_type = "application/json" + item_type = payload_type + + return {media_type: self.build_media_type(item_type, examples)} + + def build_media_type( + self, item_type: type, examples: Optional[List[Any]] = None + ) -> MediaType: + schema = self.schema_builder.classdef_to_ref(item_type) + if self.schema_transformer: + schema_transformer: Callable[[SchemaOrRef], SchemaOrRef] = self.schema_transformer # type: ignore + schema = schema_transformer(schema) + + if not examples: + return MediaType(schema=schema) + + if len(examples) == 1: + return MediaType(schema=schema, example=self._build_example(examples[0])) + + return MediaType( + schema=schema, + examples=self._build_examples(examples), + ) + + def _build_examples( + self, examples: List[Any] + ) -> Dict[str, Union[Example, ExampleRef]]: + "Creates a set of several examples for a media type." + + if self.sample_transformer: + sample_transformer: Callable[[JsonType], JsonType] = self.sample_transformer # type: ignore + else: + sample_transformer = lambda sample: sample + + results: Dict[str, Union[Example, ExampleRef]] = {} + for example in examples: + value = sample_transformer(object_to_json(example)) + + hash_string = ( + hashlib.md5(json_dump_string(value).encode("utf-8")).digest().hex() + ) + name = f"ex-{hash_string}" + + results[name] = Example(value=value) + + return results + + def _build_example(self, example: Any) -> Any: + "Creates a single example for a media type." + + if self.sample_transformer: + sample_transformer: Callable[[JsonType], JsonType] = self.sample_transformer # type: ignore + else: + sample_transformer = lambda sample: sample + + return sample_transformer(object_to_json(example)) + + +@dataclass +class ResponseOptions: + """ + Configuration options for building a response for an operation. + + :param type_descriptions: Maps each response type to a textual description (if available). + :param examples: A list of response examples. + :param status_catalog: Maps each response type to an HTTP status code. + :param default_status_code: HTTP status code assigned to responses that have no mapping. + """ + + type_descriptions: Dict[type, str] + examples: Optional[List[Any]] + status_catalog: Dict[type, HTTPStatusCode] + default_status_code: HTTPStatusCode + + +@dataclass +class StatusResponse: + status_code: str + types: List[type] = dataclasses.field(default_factory=list) + examples: List[Any] = dataclasses.field(default_factory=list) + + +class ResponseBuilder: + content_builder: ContentBuilder + + def __init__(self, content_builder: ContentBuilder) -> None: + self.content_builder = content_builder + + def _get_status_responses( + self, options: ResponseOptions + ) -> Dict[str, StatusResponse]: + status_responses: Dict[str, StatusResponse] = {} + + for response_type in options.type_descriptions.keys(): + status_code = http_status_to_string( + options.status_catalog.get(response_type, options.default_status_code) + ) + + # look up response for status code + if status_code not in status_responses: + status_responses[status_code] = StatusResponse(status_code) + status_response = status_responses[status_code] + + # append response types that are assigned the given status code + status_response.types.append(response_type) + + # append examples that have the matching response type + if options.examples: + status_response.examples.extend( + example + for example in options.examples + if isinstance(example, response_type) + ) + + return dict(sorted(status_responses.items())) + + def build_response( + self, options: ResponseOptions + ) -> Dict[str, Union[Response, ResponseRef]]: + """ + Groups responses that have the same status code. + """ + + responses: Dict[str, Union[Response, ResponseRef]] = {} + status_responses = self._get_status_responses(options) + for status_code, status_response in status_responses.items(): + response_types = tuple(status_response.types) + if len(response_types) > 1: + composite_response_type: type = Union[response_types] # type: ignore + else: + (response_type,) = response_types + composite_response_type = response_type + + description = " **OR** ".join( + filter( + None, + ( + options.type_descriptions[response_type] + for response_type in response_types + ), + ) + ) + + responses[status_code] = self._build_response( + response_type=composite_response_type, + description=description, + examples=status_response.examples or None, + ) + + return responses + + def _build_response( + self, + response_type: type, + description: str, + examples: Optional[List[Any]] = None, + ) -> Response: + "Creates a response subtree." + + if response_type is not None: + return Response( + description=description, + content=self.content_builder.build_content(response_type, examples), + ) + else: + return Response(description=description) + + +def schema_error_wrapper(schema: SchemaOrRef) -> Schema: + "Wraps an error output schema into a top-level error schema." + + return { + "type": "object", + "properties": { + "error": schema, # type: ignore + }, + "additionalProperties": False, + "required": [ + "error", + ], + } + + +def sample_error_wrapper(error: JsonType) -> JsonType: + "Wraps an error output sample into a top-level error sample." + + return {"error": error} + + +class Generator: + endpoint: type + options: Options + schema_builder: SchemaBuilder + responses: Dict[str, Response] + + def __init__(self, endpoint: type, options: Options) -> None: + self.endpoint = endpoint + self.options = options + schema_generator = JsonSchemaGenerator( + SchemaOptions( + definitions_path="#/components/schemas/", + use_examples=self.options.use_examples, + property_description_fun=options.property_description_fun, + ) + ) + self.schema_builder = SchemaBuilder(schema_generator) + self.responses = {} + + def _build_type_tag(self, ref: str, schema: Schema) -> Tag: + definition = f'' + title = typing.cast(str, schema.get("title")) + description = typing.cast(str, schema.get("description")) + return Tag( + name=ref, + description="\n\n".join( + s for s in (title, description, definition) if s is not None + ), + ) + + def _build_extra_tag_groups( + self, extra_types: Dict[str, List[type]] + ) -> Dict[str, List[Tag]]: + """ + Creates a dictionary of tag group captions as keys, and tag lists as values. + + :param extra_types: A dictionary of type categories and list of types in that category. + """ + + extra_tags: Dict[str, List[Tag]] = {} + + for category_name, category_items in extra_types.items(): + tag_list: List[Tag] = [] + + for extra_type in category_items: + name = python_type_to_name(extra_type) + schema = self.schema_builder.classdef_to_named_schema(name, extra_type) + tag_list.append(self._build_type_tag(name, schema)) + + if tag_list: + extra_tags[category_name] = tag_list + + return extra_tags + + def _build_operation(self, op: EndpointOperation) -> Operation: + doc_string = parse_type(op.func_ref) + doc_params = dict( + (param.name, param.description) for param in doc_string.params.values() + ) + + # parameters passed in URL component path + path_parameters = [ + Parameter( + name=param_name, + in_=ParameterLocation.Path, + description=doc_params.get(param_name), + required=True, + schema=self.schema_builder.classdef_to_ref(param_type), + ) + for param_name, param_type in op.path_params + ] + + # parameters passed in URL component query string + query_parameters = [] + for param_name, param_type in op.query_params: + if is_type_optional(param_type): + inner_type: type = unwrap_optional_type(param_type) + required = False + else: + inner_type = param_type + required = True + + query_parameter = Parameter( + name=param_name, + in_=ParameterLocation.Query, + description=doc_params.get(param_name), + required=required, + schema=self.schema_builder.classdef_to_ref(inner_type), + ) + query_parameters.append(query_parameter) + + # parameters passed anywhere + parameters = path_parameters + query_parameters + + # data passed in payload + if op.request_params: + builder = ContentBuilder(self.schema_builder) + if len(op.request_params) == 1: + request_name, request_type = op.request_params[0] + else: + from dataclasses import make_dataclass + + op_name = "".join(word.capitalize() for word in op.name.split("_")) + request_name = f"{op_name}Request" + request_type = make_dataclass(request_name, op.request_params) + + requestBody = RequestBody( + content={ + "application/json": builder.build_media_type( + request_type, op.request_examples + ) + }, + description=doc_params.get(request_name), + required=True, + ) + else: + requestBody = None + + # success response types + if doc_string.returns is None and is_type_union(op.response_type): + # split union of return types into a list of response types + success_type_docstring: Dict[type, Docstring] = { + typing.cast(type, item): parse_type(item) + for item in unwrap_union_types(op.response_type) + } + success_type_descriptions = { + item: doc_string.short_description + for item, doc_string in success_type_docstring.items() + if doc_string.short_description + } + else: + # use return type as a single response type + success_type_descriptions = { + op.response_type: ( + doc_string.returns.description if doc_string.returns else "OK" + ) + } + + response_examples = op.response_examples or [] + success_examples = [ + example + for example in response_examples + if not isinstance(example, Exception) + ] + + content_builder = ContentBuilder(self.schema_builder) + response_builder = ResponseBuilder(content_builder) + response_options = ResponseOptions( + success_type_descriptions, + success_examples if self.options.use_examples else None, + self.options.success_responses, + "200", + ) + responses = response_builder.build_response(response_options) + + # failure response types + if doc_string.raises: + exception_types: Dict[type, str] = { + item.raise_type: item.description for item in doc_string.raises.values() + } + exception_examples = [ + example + for example in response_examples + if isinstance(example, Exception) + ] + + if self.options.error_wrapper: + schema_transformer = schema_error_wrapper + sample_transformer = sample_error_wrapper + else: + schema_transformer = None + sample_transformer = None + + content_builder = ContentBuilder( + self.schema_builder, + schema_transformer=schema_transformer, + sample_transformer=sample_transformer, + ) + response_builder = ResponseBuilder(content_builder) + response_options = ResponseOptions( + exception_types, + exception_examples if self.options.use_examples else None, + self.options.error_responses, + "500", + ) + responses.update(response_builder.build_response(response_options)) + + if op.event_type is not None: + builder = ContentBuilder(self.schema_builder) + callbacks = { + f"{op.func_name}_callback": { + "{$request.query.callback}": PathItem( + post=Operation( + requestBody=RequestBody( + content=builder.build_content(op.event_type) + ), + responses={"200": Response(description="OK")}, + ) + ) + } + } + + else: + callbacks = None + + return Operation( + tags=[op.defining_class.__name__], + summary=doc_string.short_description, + description=doc_string.long_description, + parameters=parameters, + requestBody=requestBody, + responses=responses, + callbacks=callbacks, + security=[] if op.public else None, + ) + + def generate(self) -> Document: + paths: Dict[str, PathItem] = {} + endpoint_classes: Set[type] = set() + for op in get_endpoint_operations( + self.endpoint, use_examples=self.options.use_examples + ): + endpoint_classes.add(op.defining_class) + + operation = self._build_operation(op) + + if op.http_method is HTTPMethod.GET: + pathItem = PathItem(get=operation) + elif op.http_method is HTTPMethod.PUT: + pathItem = PathItem(put=operation) + elif op.http_method is HTTPMethod.POST: + pathItem = PathItem(post=operation) + elif op.http_method is HTTPMethod.DELETE: + pathItem = PathItem(delete=operation) + elif op.http_method is HTTPMethod.PATCH: + pathItem = PathItem(patch=operation) + else: + raise NotImplementedError(f"unknown HTTP method: {op.http_method}") + + route = op.get_route() + if route in paths: + paths[route].update(pathItem) + else: + paths[route] = pathItem + + operation_tags: List[Tag] = [] + for cls in endpoint_classes: + doc_string = parse_type(cls) + operation_tags.append( + Tag( + name=cls.__name__, + description=doc_string.long_description, + displayName=doc_string.short_description, + ) + ) + + # types that are produced/consumed by operations + type_tags = [ + self._build_type_tag(ref, schema) + for ref, schema in self.schema_builder.schemas.items() + ] + + # types that are emitted by events + event_tags: List[Tag] = [] + events = get_endpoint_events(self.endpoint) + for ref, event_type in events.items(): + event_schema = self.schema_builder.classdef_to_named_schema(ref, event_type) + event_tags.append(self._build_type_tag(ref, event_schema)) + + # types that are explicitly declared + extra_tag_groups: Dict[str, List[Tag]] = {} + if self.options.extra_types is not None: + if isinstance(self.options.extra_types, list): + extra_tag_groups = self._build_extra_tag_groups( + {"AdditionalTypes": self.options.extra_types} + ) + elif isinstance(self.options.extra_types, dict): + extra_tag_groups = self._build_extra_tag_groups( + self.options.extra_types + ) + else: + raise TypeError( + f"type mismatch for collection of extra types: {type(self.options.extra_types)}" + ) + + # list all operations and types + tags: List[Tag] = [] + tags.extend(operation_tags) + tags.extend(type_tags) + tags.extend(event_tags) + for extra_tag_group in extra_tag_groups.values(): + tags.extend(extra_tag_group) + + tag_groups = [] + if operation_tags: + tag_groups.append( + TagGroup( + name=self.options.map("Operations"), + tags=sorted(tag.name for tag in operation_tags), + ) + ) + if type_tags: + tag_groups.append( + TagGroup( + name=self.options.map("Types"), + tags=sorted(tag.name for tag in type_tags), + ) + ) + if event_tags: + tag_groups.append( + TagGroup( + name=self.options.map("Events"), + tags=sorted(tag.name for tag in event_tags), + ) + ) + for caption, extra_tag_group in extra_tag_groups.items(): + tag_groups.append( + TagGroup( + name=self.options.map(caption), + tags=sorted(tag.name for tag in extra_tag_group), + ) + ) + + if self.options.default_security_scheme: + securitySchemes = {"Default": self.options.default_security_scheme} + else: + securitySchemes = None + + return Document( + openapi=".".join(str(item) for item in self.options.version), + info=self.options.info, + jsonSchemaDialect=( + "https://json-schema.org/draft/2020-12/schema" + if self.options.version >= (3, 1, 0) + else None + ), + servers=[self.options.server], + paths=paths, + components=Components( + schemas=self.schema_builder.schemas, + responses=self.responses, + securitySchemes=securitySchemes, + ), + security=[{"Default": []}], + tags=tags, + tagGroups=tag_groups, + ) diff --git a/rfcs/openapi_generator/pyopenapi/operations.py b/rfcs/openapi_generator/pyopenapi/operations.py new file mode 100644 index 000000000..153a6b12a --- /dev/null +++ b/rfcs/openapi_generator/pyopenapi/operations.py @@ -0,0 +1,386 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the terms described in the LICENSE file in +# the root directory of this source tree. + +import collections.abc +import enum +import inspect +import typing +import uuid +from dataclasses import dataclass +from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Union + +from strong_typing.inspection import ( + get_signature, + is_type_enum, + is_type_optional, + unwrap_optional_type, +) +from termcolor import colored + + +def split_prefix( + s: str, sep: str, prefix: Union[str, Iterable[str]] +) -> Tuple[Optional[str], str]: + """ + Recognizes a prefix at the beginning of a string. + + :param s: The string to check. + :param sep: A separator between (one of) the prefix(es) and the rest of the string. + :param prefix: A string or a set of strings to identify as a prefix. + :return: A tuple of the recognized prefix (if any) and the rest of the string excluding the separator (or the entire string). + """ + + if isinstance(prefix, str): + if s.startswith(prefix + sep): + return prefix, s[len(prefix) + len(sep) :] + else: + return None, s + + for p in prefix: + if s.startswith(p + sep): + return p, s[len(p) + len(sep) :] + + return None, s + + +def _get_annotation_type(annotation: Union[type, str], callable: Callable) -> type: + "Maps a stringized reference to a type, as if using `from __future__ import annotations`." + + if isinstance(annotation, str): + return eval(annotation, callable.__globals__) + else: + return annotation + + +class HTTPMethod(enum.Enum): + "HTTP method used to invoke an endpoint operation." + + GET = "GET" + POST = "POST" + PUT = "PUT" + DELETE = "DELETE" + PATCH = "PATCH" + + +OperationParameter = Tuple[str, type] + + +class ValidationError(TypeError): + pass + + +@dataclass +class EndpointOperation: + """ + Type information and metadata associated with an endpoint operation. + + "param defining_class: The most specific class that defines the endpoint operation. + :param name: The short name of the endpoint operation. + :param func_name: The name of the function to invoke when the operation is triggered. + :param func_ref: The callable to invoke when the operation is triggered. + :param route: A custom route string assigned to the operation. + :param path_params: Parameters of the operation signature that are passed in the path component of the URL string. + :param query_params: Parameters of the operation signature that are passed in the query string as `key=value` pairs. + :param request_params: The parameter that corresponds to the data transmitted in the request body. + :param event_type: The Python type of the data that is transmitted out-of-band (e.g. via websockets) while the operation is in progress. + :param response_type: The Python type of the data that is transmitted in the response body. + :param http_method: The HTTP method used to invoke the endpoint such as POST, GET or PUT. + :param public: True if the operation can be invoked without prior authentication. + :param request_examples: Sample requests that the operation might take. + :param response_examples: Sample responses that the operation might produce. + """ + + defining_class: type + name: str + func_name: str + func_ref: Callable[..., Any] + route: Optional[str] + path_params: List[OperationParameter] + query_params: List[OperationParameter] + request_params: Optional[OperationParameter] + event_type: Optional[type] + response_type: type + http_method: HTTPMethod + public: bool + request_examples: Optional[List[Any]] = None + response_examples: Optional[List[Any]] = None + + def get_route(self) -> str: + if self.route is not None: + return self.route + + route_parts = ["", self.name] + for param_name, _ in self.path_params: + route_parts.append("{" + param_name + "}") + return "/".join(route_parts) + + +class _FormatParameterExtractor: + "A visitor to exract parameters in a format string." + + keys: List[str] + + def __init__(self) -> None: + self.keys = [] + + def __getitem__(self, key: str) -> None: + self.keys.append(key) + return None + + +def _get_route_parameters(route: str) -> List[str]: + extractor = _FormatParameterExtractor() + route.format_map(extractor) + return extractor.keys + + +def _get_endpoint_functions( + endpoint: type, prefixes: List[str] +) -> Iterator[Tuple[str, str, str, Callable]]: + if not inspect.isclass(endpoint): + raise ValueError(f"object is not a class type: {endpoint}") + + functions = inspect.getmembers(endpoint, inspect.isfunction) + for func_name, func_ref in functions: + webmethod = getattr(func_ref, "__webmethod__", None) + if not webmethod: + continue + + print(f"Processing {colored(func_name, 'white')}...") + operation_name = func_name + if operation_name.startswith("get_") or operation_name.endswith("/get"): + prefix = "get" + elif ( + operation_name.startswith("delete_") + or operation_name.startswith("remove_") + or operation_name.endswith("/delete") + or operation_name.endswith("/remove") + ): + prefix = "delete" + else: + if webmethod.method == "GET": + prefix = "get" + elif webmethod.method == "DELETE": + prefix = "delete" + else: + # by default everything else is a POST + prefix = "post" + + yield prefix, operation_name, func_name, func_ref + + +def _get_defining_class(member_fn: str, derived_cls: type) -> type: + "Find the class in which a member function is first defined in a class inheritance hierarchy." + + # iterate in reverse member resolution order to find most specific class first + for cls in reversed(inspect.getmro(derived_cls)): + for name, _ in inspect.getmembers(cls, inspect.isfunction): + if name == member_fn: + return cls + + raise ValidationError( + f"cannot find defining class for {member_fn} in {derived_cls}" + ) + + +def get_endpoint_operations( + endpoint: type, use_examples: bool = True +) -> List[EndpointOperation]: + """ + Extracts a list of member functions in a class eligible for HTTP interface binding. + + These member functions are expected to have a signature like + ``` + async def get_object(self, uuid: str, version: int) -> Object: + ... + ``` + where the prefix `get_` translates to an HTTP GET, `object` corresponds to the name of the endpoint operation, + `uuid` and `version` are mapped to route path elements in "/object/{uuid}/{version}", and `Object` becomes + the response payload type, transmitted as an object serialized to JSON. + + If the member function has a composite class type in the argument list, it becomes the request payload type, + and the caller is expected to provide the data as serialized JSON in an HTTP POST request. + + :param endpoint: A class with member functions that can be mapped to an HTTP endpoint. + :param use_examples: Whether to return examples associated with member functions. + """ + + result = [] + + for prefix, operation_name, func_name, func_ref in _get_endpoint_functions( + endpoint, + [ + "create", + "delete", + "do", + "get", + "post", + "put", + "remove", + "set", + "update", + ], + ): + # extract routing information from function metadata + webmethod = getattr(func_ref, "__webmethod__", None) + if webmethod is not None: + route = webmethod.route + route_params = _get_route_parameters(route) if route is not None else None + public = webmethod.public + request_examples = webmethod.request_examples + response_examples = webmethod.response_examples + else: + route = None + route_params = None + public = False + request_examples = None + response_examples = None + + # inspect function signature for path and query parameters, and request/response payload type + signature = get_signature(func_ref) + + path_params = [] + query_params = [] + request_params = [] + + for param_name, parameter in signature.parameters.items(): + param_type = _get_annotation_type(parameter.annotation, func_ref) + + # omit "self" for instance methods + if param_name == "self" and param_type is inspect.Parameter.empty: + continue + + # check if all parameters have explicit type + if parameter.annotation is inspect.Parameter.empty: + raise ValidationError( + f"parameter '{param_name}' in function '{func_name}' has no type annotation" + ) + + if is_type_optional(param_type): + inner_type: type = unwrap_optional_type(param_type) + else: + inner_type = param_type + + if ( + inner_type is bool + or inner_type is int + or inner_type is float + or inner_type is str + or inner_type is uuid.UUID + or is_type_enum(inner_type) + ): + if parameter.kind == inspect.Parameter.POSITIONAL_ONLY: + if route_params is not None and param_name not in route_params: + raise ValidationError( + f"positional parameter '{param_name}' absent from user-defined route '{route}' for function '{func_name}'" + ) + + # simple type maps to route path element, e.g. /study/{uuid}/{version} + path_params.append((param_name, param_type)) + else: + if route_params is not None and param_name in route_params: + raise ValidationError( + f"query parameter '{param_name}' found in user-defined route '{route}' for function '{func_name}'" + ) + + # simple type maps to key=value pair in query string + query_params.append((param_name, param_type)) + else: + if route_params is not None and param_name in route_params: + raise ValidationError( + f"user-defined route '{route}' for function '{func_name}' has parameter '{param_name}' of composite type: {param_type}" + ) + + request_params.append((param_name, param_type)) + + # check if function has explicit return type + if signature.return_annotation is inspect.Signature.empty: + raise ValidationError( + f"function '{func_name}' has no return type annotation" + ) + + return_type = _get_annotation_type(signature.return_annotation, func_ref) + + # operations that produce events are labeled as Generator[YieldType, SendType, ReturnType] + # where YieldType is the event type, SendType is None, and ReturnType is the immediate response type to the request + if typing.get_origin(return_type) is collections.abc.Generator: + event_type, send_type, response_type = typing.get_args(return_type) + if send_type is not type(None): + raise ValidationError( + f"function '{func_name}' has a return type Generator[Y,S,R] and therefore looks like an event but has an explicit send type" + ) + else: + event_type = None + response_type = return_type + + # set HTTP request method based on type of request and presence of payload + if not request_params: + if prefix in ["delete", "remove"]: + http_method = HTTPMethod.DELETE + else: + http_method = HTTPMethod.GET + else: + if prefix == "set": + http_method = HTTPMethod.PUT + elif prefix == "update": + http_method = HTTPMethod.PATCH + else: + http_method = HTTPMethod.POST + + result.append( + EndpointOperation( + defining_class=_get_defining_class(func_name, endpoint), + name=operation_name, + func_name=func_name, + func_ref=func_ref, + route=route, + path_params=path_params, + query_params=query_params, + request_params=request_params, + event_type=event_type, + response_type=response_type, + http_method=http_method, + public=public, + request_examples=request_examples if use_examples else None, + response_examples=response_examples if use_examples else None, + ) + ) + + if not result: + raise ValidationError(f"no eligible endpoint operations in type {endpoint}") + + return result + + +def get_endpoint_events(endpoint: type) -> Dict[str, type]: + results = {} + + for decl in typing.get_type_hints(endpoint).values(): + # check if signature is Callable[...] + origin = typing.get_origin(decl) + if origin is None or not issubclass(origin, Callable): # type: ignore + continue + + # check if signature is Callable[[...], Any] + args = typing.get_args(decl) + if len(args) != 2: + continue + params_type, return_type = args + if not isinstance(params_type, list): + continue + + # check if signature is Callable[[...], None] + if not issubclass(return_type, type(None)): + continue + + # check if signature is Callable[[EventType], None] + if len(params_type) != 1: + continue + + param_type = params_type[0] + results[param_type.__name__] = param_type + + return results diff --git a/rfcs/openapi_generator/pyopenapi/options.py b/rfcs/openapi_generator/pyopenapi/options.py new file mode 100644 index 000000000..f80da453b --- /dev/null +++ b/rfcs/openapi_generator/pyopenapi/options.py @@ -0,0 +1,75 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the terms described in the LICENSE file in +# the root directory of this source tree. + +import dataclasses +from dataclasses import dataclass +from http import HTTPStatus +from typing import Callable, ClassVar, Dict, List, Optional, Tuple, Union + +from .specification import ( + Info, + SecurityScheme, + SecuritySchemeAPI, + SecuritySchemeHTTP, + SecuritySchemeOpenIDConnect, + Server, +) + +HTTPStatusCode = Union[HTTPStatus, int, str] + + +@dataclass +class Options: + """ + :param server: Base URL for the API endpoint. + :param info: Meta-information for the endpoint specification. + :param version: OpenAPI specification version as a tuple of major, minor, revision. + :param default_security_scheme: Security scheme to apply to endpoints, unless overridden on a per-endpoint basis. + :param extra_types: Extra types in addition to those found in operation signatures. Use a dictionary to group related types. + :param use_examples: Whether to emit examples for operations. + :param success_responses: Associates operation response types with HTTP status codes. + :param error_responses: Associates error response types with HTTP status codes. + :param error_wrapper: True if errors are encapsulated in an error object wrapper. + :param property_description_fun: Custom transformation function to apply to class property documentation strings. + :param captions: User-defined captions for sections such as "Operations" or "Types", and (if applicable) groups of extra types. + """ + + server: Server + info: Info + version: Tuple[int, int, int] = (3, 1, 0) + default_security_scheme: Optional[SecurityScheme] = None + extra_types: Union[List[type], Dict[str, List[type]], None] = None + use_examples: bool = True + success_responses: Dict[type, HTTPStatusCode] = dataclasses.field( + default_factory=dict + ) + error_responses: Dict[type, HTTPStatusCode] = dataclasses.field( + default_factory=dict + ) + error_wrapper: bool = False + property_description_fun: Optional[Callable[[type, str, str], str]] = None + captions: Optional[Dict[str, str]] = None + + default_captions: ClassVar[Dict[str, str]] = { + "Operations": "Operations", + "Types": "Types", + "Events": "Events", + "AdditionalTypes": "Additional types", + } + + def map(self, id: str) -> str: + "Maps a language-neutral placeholder string to language-dependent text." + + if self.captions is not None: + caption = self.captions.get(id) + if caption is not None: + return caption + + caption = self.__class__.default_captions.get(id) + if caption is not None: + return caption + + raise KeyError(f"no caption found for ID: {id}") diff --git a/rfcs/openapi_generator/pyopenapi/specification.py b/rfcs/openapi_generator/pyopenapi/specification.py new file mode 100644 index 000000000..ef1a97e67 --- /dev/null +++ b/rfcs/openapi_generator/pyopenapi/specification.py @@ -0,0 +1,258 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the terms described in the LICENSE file in +# the root directory of this source tree. + +import dataclasses +import enum +from dataclasses import dataclass +from typing import Any, ClassVar, Dict, List, Optional, Union + +from strong_typing.schema import JsonType, Schema, StrictJsonType + +URL = str + + +@dataclass +class Ref: + ref_type: ClassVar[str] + id: str + + def to_json(self) -> StrictJsonType: + return {"$ref": f"#/components/{self.ref_type}/{self.id}"} + + +@dataclass +class SchemaRef(Ref): + ref_type: ClassVar[str] = "schemas" + + +SchemaOrRef = Union[Schema, SchemaRef] + + +@dataclass +class ResponseRef(Ref): + ref_type: ClassVar[str] = "responses" + + +@dataclass +class ParameterRef(Ref): + ref_type: ClassVar[str] = "parameters" + + +@dataclass +class ExampleRef(Ref): + ref_type: ClassVar[str] = "examples" + + +@dataclass +class Contact: + name: Optional[str] = None + url: Optional[URL] = None + email: Optional[str] = None + + +@dataclass +class License: + name: str + url: Optional[URL] = None + + +@dataclass +class Info: + title: str + version: str + description: Optional[str] = None + termsOfService: Optional[str] = None + contact: Optional[Contact] = None + license: Optional[License] = None + + +@dataclass +class MediaType: + schema: Optional[SchemaOrRef] = None + example: Optional[Any] = None + examples: Optional[Dict[str, Union["Example", ExampleRef]]] = None + + +@dataclass +class RequestBody: + content: Dict[str, MediaType] + description: Optional[str] = None + required: Optional[bool] = None + + +@dataclass +class Response: + description: str + content: Optional[Dict[str, MediaType]] = None + + +class ParameterLocation(enum.Enum): + Query = "query" + Header = "header" + Path = "path" + Cookie = "cookie" + + +@dataclass +class Parameter: + name: str + in_: ParameterLocation + description: Optional[str] = None + required: Optional[bool] = None + schema: Optional[SchemaOrRef] = None + example: Optional[Any] = None + + +@dataclass +class Operation: + responses: Dict[str, Union[Response, ResponseRef]] + tags: Optional[List[str]] = None + summary: Optional[str] = None + description: Optional[str] = None + operationId: Optional[str] = None + parameters: Optional[List[Parameter]] = None + requestBody: Optional[RequestBody] = None + callbacks: Optional[Dict[str, "Callback"]] = None + security: Optional[List["SecurityRequirement"]] = None + + +@dataclass +class PathItem: + summary: Optional[str] = None + description: Optional[str] = None + get: Optional[Operation] = None + put: Optional[Operation] = None + post: Optional[Operation] = None + delete: Optional[Operation] = None + options: Optional[Operation] = None + head: Optional[Operation] = None + patch: Optional[Operation] = None + trace: Optional[Operation] = None + + def update(self, other: "PathItem") -> None: + "Merges another instance of this class into this object." + + for field in dataclasses.fields(self.__class__): + value = getattr(other, field.name) + if value is not None: + setattr(self, field.name, value) + + +# maps run-time expressions such as "$request.body#/url" to path items +Callback = Dict[str, PathItem] + + +@dataclass +class Example: + summary: Optional[str] = None + description: Optional[str] = None + value: Optional[Any] = None + externalValue: Optional[URL] = None + + +@dataclass +class Server: + url: URL + description: Optional[str] = None + + +class SecuritySchemeType(enum.Enum): + ApiKey = "apiKey" + HTTP = "http" + OAuth2 = "oauth2" + OpenIDConnect = "openIdConnect" + + +@dataclass +class SecurityScheme: + type: SecuritySchemeType + description: str + + +@dataclass(init=False) +class SecuritySchemeAPI(SecurityScheme): + name: str + in_: ParameterLocation + + def __init__(self, description: str, name: str, in_: ParameterLocation) -> None: + super().__init__(SecuritySchemeType.ApiKey, description) + self.name = name + self.in_ = in_ + + +@dataclass(init=False) +class SecuritySchemeHTTP(SecurityScheme): + scheme: str + bearerFormat: Optional[str] = None + + def __init__( + self, description: str, scheme: str, bearerFormat: Optional[str] = None + ) -> None: + super().__init__(SecuritySchemeType.HTTP, description) + self.scheme = scheme + self.bearerFormat = bearerFormat + + +@dataclass(init=False) +class SecuritySchemeOpenIDConnect(SecurityScheme): + openIdConnectUrl: str + + def __init__(self, description: str, openIdConnectUrl: str) -> None: + super().__init__(SecuritySchemeType.OpenIDConnect, description) + self.openIdConnectUrl = openIdConnectUrl + + +@dataclass +class Components: + schemas: Optional[Dict[str, Schema]] = None + responses: Optional[Dict[str, Response]] = None + parameters: Optional[Dict[str, Parameter]] = None + examples: Optional[Dict[str, Example]] = None + requestBodies: Optional[Dict[str, RequestBody]] = None + securitySchemes: Optional[Dict[str, SecurityScheme]] = None + callbacks: Optional[Dict[str, Callback]] = None + + +SecurityScope = str +SecurityRequirement = Dict[str, List[SecurityScope]] + + +@dataclass +class Tag: + name: str + description: Optional[str] = None + displayName: Optional[str] = None + + +@dataclass +class TagGroup: + """ + A ReDoc extension to provide information about groups of tags. + + Exposed via the vendor-specific property "x-tagGroups" of the top-level object. + """ + + name: str + tags: List[str] + + +@dataclass +class Document: + """ + This class is a Python dataclass adaptation of the OpenAPI Specification. + + For details, see + """ + + openapi: str + info: Info + servers: List[Server] + paths: Dict[str, PathItem] + jsonSchemaDialect: Optional[str] = None + components: Optional[Components] = None + security: Optional[List[SecurityRequirement]] = None + tags: Optional[List[Tag]] = None + tagGroups: Optional[List[TagGroup]] = None diff --git a/rfcs/openapi_generator/pyopenapi/template.html b/rfcs/openapi_generator/pyopenapi/template.html new file mode 100644 index 000000000..67d4b303d --- /dev/null +++ b/rfcs/openapi_generator/pyopenapi/template.html @@ -0,0 +1,41 @@ + + + + + + + OpenAPI specification + + + + + + + +
+ + + diff --git a/rfcs/openapi_generator/pyopenapi/utility.py b/rfcs/openapi_generator/pyopenapi/utility.py new file mode 100644 index 000000000..849ce7b97 --- /dev/null +++ b/rfcs/openapi_generator/pyopenapi/utility.py @@ -0,0 +1,116 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the terms described in the LICENSE file in +# the root directory of this source tree. + +import json +import typing +from pathlib import Path +from typing import TextIO + +from strong_typing.schema import object_to_json, StrictJsonType + +from .generator import Generator +from .options import Options +from .specification import Document + + +THIS_DIR = Path(__file__).parent + + +class Specification: + document: Document + + def __init__(self, endpoint: type, options: Options): + generator = Generator(endpoint, options) + self.document = generator.generate() + + def get_json(self) -> StrictJsonType: + """ + Returns the OpenAPI specification as a Python data type (e.g. `dict` for an object, `list` for an array). + + The result can be serialized to a JSON string with `json.dump` or `json.dumps`. + """ + + json_doc = typing.cast(StrictJsonType, object_to_json(self.document)) + + if isinstance(json_doc, dict): + # rename vendor-specific properties + tag_groups = json_doc.pop("tagGroups", None) + if tag_groups: + json_doc["x-tagGroups"] = tag_groups + tags = json_doc.get("tags") + if tags and isinstance(tags, list): + for tag in tags: + if not isinstance(tag, dict): + continue + + display_name = tag.pop("displayName", None) + if display_name: + tag["x-displayName"] = display_name + + return json_doc + + def get_json_string(self, pretty_print: bool = False) -> str: + """ + Returns the OpenAPI specification as a JSON string. + + :param pretty_print: Whether to use line indents to beautify the output. + """ + + json_doc = self.get_json() + if pretty_print: + return json.dumps( + json_doc, check_circular=False, ensure_ascii=False, indent=4 + ) + else: + return json.dumps( + json_doc, + check_circular=False, + ensure_ascii=False, + separators=(",", ":"), + ) + + def write_json(self, f: TextIO, pretty_print: bool = False) -> None: + """ + Writes the OpenAPI specification to a file as a JSON string. + + :param pretty_print: Whether to use line indents to beautify the output. + """ + + json_doc = self.get_json() + if pretty_print: + json.dump( + json_doc, + f, + check_circular=False, + ensure_ascii=False, + indent=4, + ) + else: + json.dump( + json_doc, + f, + check_circular=False, + ensure_ascii=False, + separators=(",", ":"), + ) + + def write_html(self, f: TextIO, pretty_print: bool = False) -> None: + """ + Creates a stand-alone HTML page for the OpenAPI specification with ReDoc. + + :param pretty_print: Whether to use line indents to beautify the JSON string in the HTML file. + """ + + path = THIS_DIR / "template.html" + with path.open(encoding="utf-8", errors="strict") as html_template_file: + html_template = html_template_file.read() + + html = html_template.replace( + "{ /* OPENAPI_SPECIFICATION */ }", + self.get_json_string(pretty_print=pretty_print), + ) + + f.write(html) diff --git a/rfcs/openapi_generator/run_openapi_generator.sh b/rfcs/openapi_generator/run_openapi_generator.sh index 49a93f362..cf4265ae5 100755 --- a/rfcs/openapi_generator/run_openapi_generator.sh +++ b/rfcs/openapi_generator/run_openapi_generator.sh @@ -1,6 +1,5 @@ #!/bin/bash - # Copyright (c) Meta Platforms, Inc. and affiliates. # All rights reserved. # @@ -14,12 +13,11 @@ set -euo pipefail missing_packages=() check_package() { - if ! pip show "$1" &> /dev/null; then + if ! pip show "$1" &>/dev/null; then missing_packages+=("$1") fi } -check_package python-openapi check_package json-strong-typing if [ ${#missing_packages[@]} -ne 0 ]; then