Skip to content
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
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
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 Apr 10, 2024
88946af
Added script to generate Endpoint.py file from Path objects in OpenAP…
shivahari Apr 10, 2024
bfb3cc0
Updated docstring for the generic method
shivahari Apr 10, 2024
738fc15
Added dependencies to run the generate Endpoint from spec script
shivahari Apr 11, 2024
146ecb2
Fixed codacy issues
shivahari Apr 15, 2024
a929c2d
Added pylint check disable statements
shivahari Apr 16, 2024
04df072
Renamed the Jinja2 template
shivahari Apr 22, 2024
68daee9
Modified the script to handle form type request body
shivahari Apr 22, 2024
b238ca1
Removed redundant logging statement
shivahari Apr 22, 2024
25b1a16
Updated docstring
shivahari Apr 22, 2024
01cc94f
Updated the openapi3_parser module version
shivahari Apr 24, 2024
b12eb6a
Modified the generate Endpoint script to check if URL versioning is p…
shivahari May 6, 2024
36eac65
Modified the Interface & Player modules to use the Cars_Endpoint modu…
shivahari May 15, 2024
c1b96bd
Replaced the old Endpoint module with the one generated using the end…
shivahari May 15, 2024
e531a35
Added API auto generator project directory
shivahari May 15, 2024
6be7f43
Improved exception handling and logging
shivahari May 15, 2024
086915d
Improved exception handling and logging
shivahari May 15, 2024
9106dff
Replace comments with multiline comment
shivahari Jun 11, 2024
2d3b9c7
Merge branch 'master' into restructure_endpoint_generator
shivahari Oct 30, 2024
bbd70a2
Removed script generated Endpoint file and undid changes made in Play…
shivahari Oct 30, 2024
d3c3ec9
Modified the endpoint generator modules to accommodate latest changes…
shivahari Oct 30, 2024
7a87bf6
Added openapi_parser module
shivahari Oct 30, 2024
3001922
Added new generic method to make HTTP requests
shivahari Oct 30, 2024
9eafec8
Added statement to ignore pylint function too-complex error
shivahari Oct 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions api_auto_generator/Endpoint_Generator.md
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

Copy link
Collaborator

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"

## 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
100 changes: 100 additions & 0 deletions api_auto_generator/endpoint_module_generator.py
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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
97 changes: 97 additions & 0 deletions api_auto_generator/endpoint_name_generator.py
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,
Copy link
Collaborator

Choose a reason for hiding this comment

The 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,)
Loading