apidoc2/libraries/apidoc.py
Antonio de la Rosa aaf3c17220 Added files
2025-04-26 16:50:27 +02:00

442 lines
14 KiB
Python

import inspect
import re
from typing import Annotated, get_type_hints
from paramecio2.libraries.responsesapi import ListItem
from settings import config
wsgi_type='flask'
if hasattr(config, 'wsgi_type'):
wsgi_type=config.wsgi_type
if wsgi_type=='flask':
from flask import current_app
#from app import app
"""
import app
for key, view in app.app.view_functions.items():
#print(key)
#print(url_for(key), key, view)
if key.find('.')!=-1:
#print(url_for(key))
print(rule, view)
for rule in app.app.url_map.iter_rules():
print(rule.endpoint, rule)
"""
class AppDoc:
def __init__(self, app, api_description):
self.routes={}
self.description=api_description
self.tags={}
self.components={}
arr_apidocs=[]
arr_components=[]
rules={}
if wsgi_type=='flask':
for rule in current_app.url_map.iter_rules():
#print(rule.endpoint, rule)
if rule.endpoint.startswith(app.name+'.'):
rules[rule.endpoint]=rule
#print(rules)
for key, view in current_app.view_functions.items():
#print(app.name)
#print(key)
if key.startswith(app.name+'.'):
#print(view.get_callback_args())
#if 'tag' in inspect.getmembers(view, lambda a:not(inspect.isroutine(a))):
# print(key)
if 'tag' in list(inspect.signature(view).parameters.keys()):
route=re.sub(r'<\w+[:](\w+)?>', '{\\1}', rules[key].rule)
#print(route)
if 'GET' in rules[key].methods:
method='get'
if 'POST' in rules[key].methods:
method='post'
if 'PUT' in rules[key].methods:
method='put'
if 'DELETE' in rules[key].methods:
method='delete'
apidoc=ApiDoc(view, route, method, 'Action summary', 'Description')
self.routes[key]=apidoc
if not apidoc.tag in arr_apidocs:
arr_apidocs.append(apidoc.tag)
self.tags[apidoc.tag]=apidoc.tag.capitalize()
if len(apidoc.component)>0:
self.components.update(apidoc.component)
type_hints=get_type_hints(view)
if 'return' in type_hints:
return_obj=type_hints['return']
name_comp=return_obj.__name__
self.components=get_fields(return_obj, name_comp, self.components)
else:
for entry in app.routes:
#print(entry.args)
# Function is added to doc api if tag inside
if 'tag' in entry.get_callback_args():
route=re.sub(r'<(\w+)[:]?.*?>', '{\\1}', entry.rule)
#print(entry.name)
#routes.append({'route': orule, 'method': entry.method.lower(), 'api_description': api_description, 'tag': tag})
method=entry.method.lower()
apidoc=ApiDoc(entry.callback, route, method, 'Action summary', 'Description')
self.routes[entry.name]=apidoc
#{'name': 'apidoc', 'description': 'apidoc description'}
if not apidoc.tag in arr_apidocs:
arr_apidocs.append(apidoc.tag)
self.tags[apidoc.tag]=apidoc.tag.capitalize()
if len(apidoc.component)>0:
#print(apidoc.component)
self.components.update(apidoc.component)
#if not apidoc.components
# Get return from route function called entry.callback
type_hints=get_type_hints(entry.callback)
if 'return' in type_hints:
return_obj=type_hints['return']
name_comp=return_obj.__name__
#fields_return=inspect.getmembers(return_obj, lambda a:not(inspect.isroutine(a)))
self.components=get_fields(return_obj, name_comp, self.components)
def get_fields(return_obj, name_comp, components):
if not name_comp in components and name_comp!='StandardResponse':
components[name_comp]={}
fields_return=inspect.getmembers(return_obj, lambda a:not(inspect.isroutine(a)))
for f in [field for field in fields_return if not field[0].startswith('__')]:
name_prop=f[0]
if type(f[1]).__bases__[0] is not ListItem:
components[name_comp][name_prop]={}
components[name_comp][name_prop]['type']=getattr(f[1], 'jtype')
try:
components[name_comp][name_prop]['format']=getattr(f[1], 'jformat')
except AttributeError:
components[name_comp][name_prop]['format']=''
try:
components[name_comp][name_prop]['example']=getattr(f[1], 'jexample')
except AttributeError:
components[name_comp][name_prop]['example']=''
else:
#components=get_fields(f[1]
components[name_comp][name_prop]={}
components[name_comp][name_prop]['type']='array'
name_comp_sub=type(f[1]).__name__
components[name_comp][name_prop]['object']=name_comp_sub
components=get_fields(f[1], name_comp_sub, components)
pass
return components
#print(self.tags)
class ApiDoc:
def __init__(self, func_api, route, method, summary, description):
self.route=route
self.method=method
self.summary=summary
self.description=description
#self.tag_name=tag_name
self.operationId="".join([f.capitalize() for f in func_api.__name__.split('_')])
self.parameters=[]
self.tag=''
self.bearer=False
self.post={}
sign=inspect.signature(func_api)
# The return object
self.return_obj=None
self.component={}
#if hasattr(sign, 'return_annotation'):
if sign.return_annotation.__name__!='_empty':
self.return_obj=sign.return_annotation.__name__
#print(self.return_obj)
args=inspect.getfullargspec(func_api)
annotations=func_api.__annotations__
# The parameters
#print(annotations)
params=sign.parameters
#print(params)
args_len=len(tuple(re.finditer(r'\{(\w+)[:]?.*?\}', route)))
#print(args_len)
z=0
for k, f in params.items():
if k!='tag' and k!='post' and z<args_len:
required='false'
#print(type(f).__name__)
if type(f.default).__name__=='type':
required='true'
description=''
if k in annotations:
"""
for property, value in vars(annotations[k]).items():
print(property, ":", value)
"""
#print(annotations[k].__name__)
if annotations[k].__name__=='Annotated':
description=annotations[k].__metadata__[0]
type_arg=annotations[k].__args__[0].__name__
else:
description=k.capitalize()
type_arg=annotations[k].__name__
else:
description=k.capitalize()
type_arg='string'
if type_arg=='int':
type_arg='integer'
if type_arg=='str':
type_arg='string'
self.parameters.append({'name': k, 'in': 'path', 'description': description, 'required': required, 'schema': {'type': type_arg}})
z+=1
if 'tag' in params:
self.tag=params['tag'].default
if 'bearer' in params:
self.bearer=True
# Set json object if method=post
if method=='post' or method=='put':
#print(params)
if 'post' in annotations:
#print(params['post'])
#print(annotations['post'])
self.post['description']=annotations['post'].__metadata__[0]
self.post['content']=annotations['post'].__args__[0].__name__
# Add to self.component
post_obj=annotations['post'].__args__[0]
name_comp=post_obj.__name__
self.component[name_comp]={}
fields_post=inspect.getmembers(post_obj, lambda a:not(inspect.isroutine(a)))
for f in [field for field in fields_post if not field[0].startswith('__')]:
name_prop=f[0]
self.component[name_comp][name_prop]={}
self.component[name_comp][name_prop]['type']=getattr(f[1], 'jtype')
try:
self.component[name_comp][name_prop]['format']=getattr(f[1], 'jformat')
except AttributeError:
self.component[name_comp][name_prop]['format']=''
try:
self.component[name_comp][name_prop]['example']=getattr(f[1], 'jexample')
except AttributeError:
self.component[name_comp][name_prop]['example']=''
pass
#self.post['description']=
"""
requestBody:
description: Create a new pet in the store
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
"""
pass
#print(get_type_hints(func_api, include_extras=True))
# api_description=metadata.default['api_description']
#print(self.parameters)
#for k,f in params:
#metadata=signature.parameters.get('metadata')
self.response={}
self.response[200]={'description': 'successful operation'}
self.response[400]={'description': 'Invalid ID supplied'}
self.response[404]={'description': 'Element not found'}
def add_parameter(self, name, in_type, description, required, type_arg):
self.parameters.append({'name': name, 'in': in_type, 'description': description, 'required': required, 'schema': {'type': type_arg}})
class JsonObject:
def toJSON(self):
return json.dumps(
self,
default=lambda o: o.__dict__,
sort_keys=True,
indent=4)
class Schema:
properties=[]
class Property:
name=''
type=''
format=''
example=''
# If type==array you neeed fill items.
items=None
"""
Item:
type: object
properties:
reference:
type: string
part_number:
type: string
name:
type: string
manufacturer:
type: string
description:
type: string
unit_price:
type: number
format: float
category:
type: string
family:
type: string
accounting_account:
type: string
discount:
type: number
format: float
tax_type:
type: number
format: float
tax_total:
type: number
format: float
subtotal:
type: number
format: float
total:
type: number
format: float
serial_numbers:
type: array
items:
type: string
"""
"""
% for route in routes:
'${route['route']}':
${route['method']}:
tags:
- item
summary: Find item by ID
description: Returns a single item
operationId: getItemById
parameters:
- name: itemId
in: path
description: ID of item to return
required: true
schema:
type: integer
format: int64
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Item'
'400':
description: Invalid ID supplied
'404':
description: Pet not found
security:
- api_key: []
% endfor
"""