├── api ├── __init__.py ├── datatypes.py └── api_generator.py ├── .gitignore ├── requirements.txt ├── README.md ├── CODE_OF_CONDUCT.md ├── app.py ├── LICENSE ├── test └── ondemand_test_call_service.py └── CONTRIBUTING.md /api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | .idea/ 3 | __pycache__/ 4 | api/__pycache__/ -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==1.0.3 2 | boto3==1.9.166 3 | dataclasses-json==0.2.4 4 | 5 | apispec==2.0.2 6 | 7 | requests==2.22.0 8 | 9 | # project health 10 | pytest==5.1.1 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Sample code: Generating REST APIs from data classes in Python 2 | 3 | Sample code supporting the `Generating REST APIs from data classes in Python` blog post. 4 | 5 | ## License 6 | 7 | This library is licensed under the MIT-0 License. See the LICENSE file. 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /api/datatypes.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | from dataclasses import dataclass 5 | 6 | from dataclasses_json import dataclass_json 7 | 8 | 9 | @dataclass 10 | @dataclass_json 11 | class Person: 12 | name: str 13 | age: int 14 | 15 | 16 | @dataclass 17 | @dataclass_json 18 | class CreatePersonRequest: 19 | person: Person 20 | 21 | 22 | @dataclass 23 | @dataclass_json 24 | class CreatePersonResponse: 25 | person_id: int 26 | 27 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import logging 5 | 6 | from flask import request, Flask 7 | 8 | from api.datatypes import CreatePersonRequest, CreatePersonResponse 9 | 10 | SERVICE_PORT = 8888 11 | 12 | app = Flask(__name__) 13 | 14 | 15 | OPERATION_CREATE_PERSON: str = 'create-person' 16 | @app.route(f'/{OPERATION_CREATE_PERSON}', methods=['POST']) 17 | def create_person(): 18 | payload = request.get_json() 19 | logging.info(f"Incoming payload for {OPERATION_CREATE_PERSON}: {payload}") 20 | 21 | person = CreatePersonRequest.from_json(payload) 22 | logging.info(f'Creating person {person}') 23 | 24 | response = CreatePersonResponse(person_id=1234) 25 | 26 | return response.to_json() 27 | 28 | 29 | if __name__ == '__main__': 30 | app.run(debug=True, port=SERVICE_PORT, host='0.0.0.0') 31 | 32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /test/ondemand_test_call_service.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | from typing import Any 5 | 6 | import requests 7 | 8 | from api.datatypes import CreatePersonRequest, Person, CreatePersonResponse 9 | from app import OPERATION_CREATE_PERSON, SERVICE_PORT 10 | 11 | 12 | def test_get_dubbing_job_status_local(): 13 | response = _send_request(secure=False, 14 | host='localhost', 15 | service_port=SERVICE_PORT, 16 | operation=OPERATION_CREATE_PERSON, 17 | request=CreatePersonRequest(Person(name='Jane Doe', age='40'))) 18 | 19 | response_obj = CreatePersonResponse.from_json(response) 20 | 21 | assert response_obj.person_id is not None 22 | 23 | 24 | def test_get_dubbing_job_status_API(): 25 | response = _send_request(secure=True, 26 | host='<>..execute-api.us-east-1.amazonaws.com', 27 | service_port=80, 28 | path='sample-generated-api', 29 | operation=OPERATION_CREATE_PERSON, 30 | request=CreatePersonRequest(Person(name='Jane Doe', age='40')), 31 | api_key='<>') 32 | 33 | response_obj = CreatePersonResponse.from_json(response) 34 | 35 | assert response_obj.person_id is not None 36 | 37 | 38 | def _send_request(secure: bool, host: str, service_port, path: str, operation: str, request:Any, api_key=None) -> str: 39 | base_url = f'{"https" if secure else "http"}://{host}:{service_port}/{path}' 40 | 41 | response = requests.post(f'{base_url}/{operation}', headers={'x-api-key': api_key}, json=request.to_json()) 42 | print("Response is: {}".format(response.text)) 43 | return response.text 44 | 45 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /api/api_generator.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | import argparse 5 | import json 6 | import logging 7 | import os 8 | 9 | import boto3 10 | from apispec import APISpec 11 | from apispec.ext.marshmallow import MarshmallowPlugin 12 | from api.datatypes import CreatePersonRequest, CreatePersonResponse 13 | from app import OPERATION_CREATE_PERSON 14 | 15 | logging.basicConfig(level=logging.INFO) 16 | logger = logging.getLogger(os.path.basename(__file__)) 17 | 18 | 19 | def find_api_id(api_gateway_client, api_name): 20 | """ 21 | :param api_gateway_client: the client to use 22 | :param api_name: the API to find 23 | :return: the API ID of a unique API name; raises an error if there none or too many APIs with that name 24 | """ 25 | # Do use a paginator if you have a large number of APIs within the same region: 26 | # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/apigateway.html#APIGateway.Paginator.GetRestApis 27 | 28 | apis = api_gateway_client.get_rest_apis() 29 | 30 | if apis['ResponseMetadata']['HTTPStatusCode'] != 200: 31 | return None 32 | 33 | api_ids = list(filter(lambda api: api['name'] == api_name, apis['items'])) 34 | 35 | if len(api_ids) > 1: 36 | raise RuntimeError(f'Too many APIs with the same name: {api_name}') 37 | elif len(api_ids) == 0: 38 | raise RuntimeError(f'No API with the name: {api_name}') 39 | 40 | return api_ids[0]['id'] 41 | 42 | 43 | def generate_open_api_definition(update: bool): 44 | api_name = "sample-generated-api" 45 | spec = APISpec( 46 | title=api_name, 47 | version="1.0.0", 48 | openapi_version="3.0.2", 49 | info=dict(description="Sample Generated API from Python code"), 50 | plugins=[MarshmallowPlugin()] 51 | ) 52 | 53 | spec.components.security_scheme("api_key", { 54 | "type": "apiKey", 55 | "name": "x-api-key", 56 | "in": "header" 57 | }) 58 | 59 | ################ 60 | generate_operation(path=OPERATION_CREATE_PERSON, 61 | request_schema=CreatePersonRequest.schema(), 62 | request_schema_name=CreatePersonRequest.__name__, 63 | response_schema=CreatePersonResponse.schema(), 64 | response_schema_name=CreatePersonResponse.__name__, 65 | spec=spec, 66 | ecs_host='http://myecshost-1234567890.us-east-1.elb.amazonaws.com') 67 | 68 | spec_dict = spec.to_dict() 69 | 70 | api_definition = json.dumps(spec_dict, indent=2) 71 | 72 | logger.info(f'API definition:\n{api_definition}') 73 | 74 | api_gateway_client = boto3.client('apigateway') 75 | 76 | if not update: 77 | logger.info(f'Creating API: {api_name}') 78 | api_gateway_client.import_rest_api( body=api_definition ) 79 | else: 80 | logger.info(f'Updating API: {api_name}') 81 | api_gateway_client.put_rest_api(body=api_definition, mode='merge', restApiId=find_api_id(api_gateway_client, api_name)) 82 | 83 | 84 | def generate_operation(path, request_schema, request_schema_name, response_schema, response_schema_name, spec, ecs_host): 85 | spec.components.schema(request_schema_name, schema=request_schema) 86 | spec.components.schema(response_schema_name, schema=response_schema) 87 | spec.path( 88 | path=f"/{path}", 89 | operations=dict( 90 | post={ 91 | "x-amazon-apigateway-auth": { 92 | "type": "NONE" 93 | }, 94 | "requestBody": {"content": {"application/json": {"schema": request_schema}}}, 95 | "responses": {"200": {"description": 'the response', "content": {"application/json": {"schema": response_schema}}}}, 96 | "security": [{"api_key": []}], 97 | 98 | # Documented in https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-integration.html 99 | "x-amazon-apigateway-integration": { 100 | "passthroughBehavior": "when_no_match", 101 | "type": "http_proxy", 102 | "httpMethod": "POST", 103 | "uri": f"{ecs_host}/{path}" 104 | } 105 | } 106 | ) 107 | ) 108 | 109 | 110 | if __name__ == '__main__': 111 | PARSER = argparse.ArgumentParser() 112 | PARSER.add_argument('--update', default=False, action='store_true', help='validates the command line parameters and shows a summary of them') 113 | 114 | ARGS = PARSER.parse_args() 115 | generate_open_api_definition(update=ARGS.update) 116 | --------------------------------------------------------------------------------