-
Notifications
You must be signed in to change notification settings - Fork 190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Api endpoint generator #494
Open
shivahari
wants to merge
24
commits into
master
Choose a base branch
from
api_endpoint_generator
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
0c4a507
Added generic method to make HTTP request against different HTTP methods
shivahari 88946af
Added script to generate Endpoint.py file from Path objects in OpenAP…
shivahari bfb3cc0
Updated docstring for the generic method
shivahari 738fc15
Added dependencies to run the generate Endpoint from spec script
shivahari 146ecb2
Fixed codacy issues
shivahari a929c2d
Added pylint check disable statements
shivahari 04df072
Renamed the Jinja2 template
shivahari 68daee9
Modified the script to handle form type request body
shivahari b238ca1
Removed redundant logging statement
shivahari 25b1a16
Updated docstring
shivahari 01cc94f
Updated the openapi3_parser module version
shivahari b12eb6a
Modified the generate Endpoint script to check if URL versioning is p…
shivahari 36eac65
Modified the Interface & Player modules to use the Cars_Endpoint modu…
shivahari c1b96bd
Replaced the old Endpoint module with the one generated using the end…
shivahari e531a35
Added API auto generator project directory
shivahari 6be7f43
Improved exception handling and logging
shivahari 086915d
Improved exception handling and logging
shivahari 9106dff
Replace comments with multiline comment
shivahari 2d3b9c7
Merge branch 'master' into restructure_endpoint_generator
shivahari bbd70a2
Removed script generated Endpoint file and undid changes made in Play…
shivahari d3c3ec9
Modified the endpoint generator modules to accommodate latest changes…
shivahari 7a87bf6
Added openapi_parser module
shivahari 3001922
Added new generic method to make HTTP requests
shivahari 9eafec8
Added statement to ignore pylint function too-complex error
shivahari File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# Auto Generate Endpoint modules for API Automation Framework # | ||
The Endpoint generator project helps automate creating API automation tests using <a href="https://qxf2.com">Qxf2's</a> <a href="https://qxf2.com/blog/easily-maintainable-api-test-automation-framework/">API Automation framework</a>. It generates Endpoint modules - an abstraction for endpoints in the application under test from an <a href="https://learn.openapis.org/introduction.html">OpenAPI specification</a>. | ||
|
||
## Requirements ## | ||
- An V3.x.x OpenAPI specification for your API app. | ||
- The spec file can be a `JSON` or `YAML` file. | ||
|
||
## How to run the script? ## | ||
- Validate the OpenAPI specification | ||
``` | ||
python api_auto_generator/endpoint_module_generator --spec <OpenAPI_spec_file_location> | ||
``` | ||
This command will help check if the OpenAPI spec can be used to generate Endpoints file. It will raise an exception for invalid or incomplete specs. | ||
- Generate the `Endpoint` module | ||
``` | ||
python api_auto_generator/endpoint_module_generator --spec <OpenAPI_spec_file_location> --generate-endpoints | ||
``` | ||
This command will generate `<endpoint_name>_Endpoint.py` module in the `endpoints` dir. | ||
|
||
## How does the script work? ## | ||
- The script uses `openapi3_parser` module to parse and read the contents from an OpenAPI spec. | ||
- The endpoints and its details are read from the spec | ||
- A module-name, class-name, instance-method-names are all generated for the endpoint | ||
- The Path & Query parameters to be passed to the Endpoint class is generated | ||
- The json/data params to be passed to the requests method is generated from the request body | ||
- A Python dictionary collecting all these values is generated | ||
- Tge generated Python dictionary is redered on a Jinja2 template | ||
|
||
## Limitations/Constraints on using the Generate Endpoint script ## | ||
|
||
### Invalid OpenAPI spec ### | ||
- The Generate Endpoint script validates the OpenAPI spec at the start of the execution, using an invalid spec triggers an exception | ||
- The JSON Schema validation is also done in step #1, but the exception raised regarding a JSON Schema error can sometimes be a little confusing, in such cases replace the failing schema with {} to proceed to generate Endpoint files | ||
|
||
### Minimal spec ### | ||
- When using a minimal spec, to check if Endpoint files can be generated from it, run the script using --spec CLI param alone, you can proceed to use --generate-endpoint param if no issue was seen with the previous step |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
""" | ||
What does this module do?: | ||
- It creates an Endpoint file with a class from the OpenAPI spec | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need ":" here? |
||
- The path key in the spec is translated to an Endpoint | ||
- The operations(http methods) for a path is translated to instance methods for the Endpoint | ||
- The parameters for operations are translated to function parameters for the instance methods | ||
- HTTP Basic, HTTP Bearer and API Keys Auth are currently supported by this module & should passed | ||
through headers | ||
""" | ||
|
||
|
||
from argparse import ArgumentParser | ||
from pathlib import Path | ||
from jinja2 import FileSystemLoader, Environment | ||
from jinja2.exceptions import TemplateNotFound | ||
from loguru import logger | ||
from openapi_spec_parser import OpenAPISpecParser | ||
|
||
|
||
# pylint: disable=line-too-long | ||
# Get the template file location & endpoint destination location relative to this script | ||
ENDPOINT_TEMPLATE_NAME = Path(__file__).parent.joinpath("templates").joinpath("endpoint_template.jinja2") # <- Jinja2 template needs to be on the same directory as this script | ||
ENDPOINT_DESTINATION_DIR = Path(__file__).parent.parent.joinpath("endpoints") # <- The Endpoint files are created in the endpoints dir in the project root | ||
|
||
|
||
class EndpointGenerator(): | ||
""" | ||
A class to Generate Endpoint module using Jinja2 template | ||
""" | ||
|
||
|
||
def __init__(self, logger_obj: logger): | ||
""" | ||
Initialize Endpoint Generator class | ||
""" | ||
self.endpoint_template_filename = ENDPOINT_TEMPLATE_NAME.name | ||
self.jinja_template_dir = ENDPOINT_TEMPLATE_NAME.parent.absolute() | ||
self.logger = logger_obj | ||
self.jinja_environment = Environment(loader=FileSystemLoader(self.jinja_template_dir), | ||
autoescape=True) | ||
|
||
|
||
def endpoint_class_content_generator(self, | ||
endpoint_class_name: str, | ||
endpoint_class_content: dict) -> str: | ||
""" | ||
Create Jinja2 template content | ||
""" | ||
content = None | ||
template = self.jinja_environment.get_template(self.endpoint_template_filename) | ||
content = template.render(class_name=endpoint_class_name, class_content=endpoint_class_content) | ||
self.logger.info(f"Rendered content for {endpoint_class_name} class using Jinja2 template") | ||
return content | ||
|
||
|
||
def generate_endpoint_file(self, | ||
endpoint_filename: str, | ||
endpoint_class_name: str, | ||
endpoint_class_content: dict): | ||
""" | ||
Create an Endpoint file | ||
""" | ||
try: | ||
endpoint_filename = ENDPOINT_DESTINATION_DIR.joinpath(endpoint_filename+'.py') | ||
endpoint_content = self.endpoint_class_content_generator(endpoint_class_name, | ||
endpoint_class_content) | ||
with open(endpoint_filename, 'w', encoding='utf-8') as endpoint_f: | ||
endpoint_f.write(endpoint_content) | ||
except TemplateNotFound: | ||
self.logger.error(f"Unable to find {ENDPOINT_TEMPLATE_NAME.absolute()}") | ||
except Exception as endpoint_creation_err: | ||
self.logger.error(f"Unable to generate Endpoint file - {endpoint_filename} due to {endpoint_creation_err}") | ||
else: | ||
self.logger.success(f"Successfully generated Endpoint file - {endpoint_filename.name}") | ||
|
||
|
||
if __name__ == "__main__": | ||
arg_parser = ArgumentParser(prog="GenerateEndpointFile", | ||
description="Generate Endpoint.py file from OpenAPI spec") | ||
arg_parser.add_argument("--spec", | ||
dest="spec_file", | ||
required=True, | ||
help="Pass the location to the OpenAPI spec file, Passing this param alone will run a dry run of endpoint content generation with actually creating the endpoint") | ||
arg_parser.add_argument("--generate-endpoints", | ||
dest='if_generate_endpoints', | ||
action='store_true', | ||
help="This param will create <endpoint_name>_endpoint.py file for Path objects from the OpenAPI spec") | ||
|
||
args = arg_parser.parse_args() | ||
try: | ||
parser = OpenAPISpecParser(args.spec_file, logger) | ||
if args.if_generate_endpoints: | ||
endpoint_generator = EndpointGenerator(logger) | ||
for module_name, file_content in parser.parsed_dict.items(): | ||
for class_name, class_content in file_content.items(): | ||
endpoint_generator.generate_endpoint_file(module_name, | ||
class_name, | ||
class_content) | ||
except Exception as ep_generation_err: | ||
raise ep_generation_err |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
""" | ||
Module to generate: | ||
1. Module name | ||
2. Class name | ||
3. Method name | ||
""" | ||
|
||
import re | ||
from typing import Union | ||
from packaging.version import Version, InvalidVersion | ||
|
||
class NameGenerator(): | ||
"Base class for generating names" | ||
|
||
def __init__(self, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we have 2 lines after class definition to keep it consistent |
||
endpoint_url: str, | ||
if_query_param: bool, | ||
path_params: list, | ||
requestbody_type: str): | ||
"Init NameGen object" | ||
self.endpoint_split, self.api_version_num = self.split_endpoint_string(endpoint_url) | ||
self.common_base = self.endpoint_split[0] | ||
self.endpoints_in_a_file = [ ep for ep in re.split("-|_", self.common_base)] | ||
self.if_query_param = if_query_param | ||
self.path_params = path_params | ||
self.requestbody_type = requestbody_type | ||
|
||
@property | ||
def module_name(self) -> str : | ||
"Module name for an Endpoint" | ||
return "_" + "_".join(self.endpoints_in_a_file) + "_" + "endpoint" | ||
|
||
@property | ||
def class_name(self) -> str : | ||
"Class name for Endpoint" | ||
capitalized_endpoints_in_a_file = [ ep.capitalize() for ep in self.endpoints_in_a_file] | ||
return "".join(capitalized_endpoints_in_a_file) + "Endpoint" | ||
|
||
@property | ||
def url_method_name(self) -> str : | ||
"URL method name for endpoint" | ||
return self.common_base.lower().replace('-', '_') + "_" + "url" | ||
|
||
@property | ||
def base_api_param_string(self) -> str : | ||
"Base API method parameter string" | ||
param_string = "" | ||
if self.if_query_param: | ||
param_string += ", params=params" | ||
if self.requestbody_type == "json": | ||
param_string += ", json=json" | ||
if self.requestbody_type == "data": | ||
param_string += ", data=data" | ||
param_string += ", headers=headers" | ||
return param_string | ||
|
||
@property | ||
def instance_method_param_string(self) -> str : | ||
"Instance method parameter string" | ||
param_string = "self" | ||
if self.if_query_param: | ||
param_string += ", params" | ||
for param in self.path_params: | ||
param_string += f", {param[0]}" | ||
if self.requestbody_type == "json": | ||
param_string += ", json" | ||
if self.requestbody_type == "data": | ||
param_string += ", data" | ||
param_string += ', headers' | ||
return param_string | ||
|
||
def get_instance_method_name(self, http_method: str) -> str : | ||
"Generate Instance method name" | ||
endpoint_split = [ ep.lower().replace('-','_') for ep in self.endpoint_split ] | ||
return http_method + "_" + "_".join(endpoint_split) | ||
|
||
def split_endpoint_string(self, endpoint_url: str) -> tuple[list[str], Union[str,None]]: | ||
""" | ||
Split the text in the endpoint, clean it up & return a list of text | ||
""" | ||
version_num = None | ||
if endpoint_url == "/": # <- if the endpoint is only / | ||
endpoint_split = ["home_base"] # <- make it /home_base (it needs to be unique) | ||
else: | ||
endpoint_split = endpoint_url.split("/") | ||
# remove {} from path paramters in endpoints | ||
endpoint_split = [ re.sub("{|}","",text) for text in endpoint_split if text ] | ||
for split_values in endpoint_split: | ||
try: | ||
if_api_version = Version(split_values) # <- check if version number present | ||
version_num = [ str(num) for num in if_api_version.release ] | ||
version_num = '_'.join(version_num) | ||
endpoint_split.remove(split_values) | ||
except InvalidVersion: | ||
if split_values == "api": | ||
endpoint_split.remove(split_values) | ||
return (endpoint_split, version_num,) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a typo in "The"