Compare commits
No commits in common. "master" and "0.6.0" have entirely different histories.
@ -6,7 +6,7 @@
|
||||
|
||||
# Official language image. Look for the different tagged releases at:
|
||||
# https://hub.docker.com/r/library/python/tags/
|
||||
image: python:alpine3.18
|
||||
image: python:3.9-bullseye
|
||||
|
||||
# Change pip's cache directory to be inside the project directory since we can
|
||||
# only cache local items.
|
||||
@ -27,64 +27,33 @@ cache:
|
||||
- .cache/pip
|
||||
- venv/
|
||||
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
|
||||
.before_script_template: &test
|
||||
before_script:
|
||||
- python3 -V # Print out python version for debugging
|
||||
- python3 -m venv /tmp/venv
|
||||
- /tmp/venv/bin/pip3 install .["tests","pyexcel"]
|
||||
- /tmp/venv/bin/pip3 install coverage pytest
|
||||
|
||||
test:
|
||||
image: python:alpine${ALPINEVERSION}
|
||||
stage: test
|
||||
<<: *test
|
||||
parallel:
|
||||
matrix:
|
||||
- ALPINEVERSION: ["3.16", "3.17", "3.18", "3.19"]
|
||||
script:
|
||||
- /tmp/venv/bin/pytest --version
|
||||
- PYTHONPATH=./tests/ /tmp/venv/bin/coverage run --source halfapi -m pytest
|
||||
- /tmp/venv/bin/coverage xml
|
||||
- /tmp/venv/bin/halfapi --version
|
||||
|
||||
artifacts:
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
|
||||
build_pypi:
|
||||
stage: build
|
||||
script:
|
||||
- apt-get update && apt-get -y install python3-venv
|
||||
- python3 -V # Print out python version for debugging
|
||||
- python3 -m venv /tmp/venv
|
||||
- /tmp/venv/bin/pip3 install .
|
||||
- pip3 install pipenv
|
||||
- pipenv install --dev --skip-lock
|
||||
- pipenv run pytest --version
|
||||
- pipenv run pytest -v
|
||||
- pipenv run halfapi --version
|
||||
|
||||
run:
|
||||
script:
|
||||
- apt-get update && apt-get -y install python3-venv
|
||||
- python3 -V # Print out python version for debugging
|
||||
- pip3 install pipenv
|
||||
- pipenv install --dev --skip-lock
|
||||
- pipenv run python -m build --sdist
|
||||
- pipenv run python -m build --wheel
|
||||
artifacts:
|
||||
paths:
|
||||
- dist/*.whl
|
||||
rules:
|
||||
- if: '$CI_COMMIT_TAG != ""'
|
||||
variables:
|
||||
TAG: $CI_COMMIT_TAG
|
||||
|
||||
build_container:
|
||||
rules:
|
||||
- if: '$CI_COMMIT_TAG != ""'
|
||||
variables:
|
||||
IMGTAG: $CI_COMMIT_TAG
|
||||
- if: '$CI_COMMIT_REF_NAME == "devel"'
|
||||
variables:
|
||||
IMGTAG: "latest"
|
||||
stage: build
|
||||
build:
|
||||
image: $CI_REGISTRY/devtools/kaniko
|
||||
script:
|
||||
- echo "Will upload image halfapi:\"$IMGTAG\""
|
||||
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
|
||||
- /kaniko/executor --force --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY/malves/halfapi:"$IMGTAG"
|
||||
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY/malves/halfapi:latest
|
||||
artifacts:
|
||||
paths:
|
||||
- /kaniko/.docker/config.json
|
||||
|
240
CHANGELOG.md
240
CHANGELOG.md
@ -1,245 +1,5 @@
|
||||
# HalfAPI
|
||||
|
||||
## 0.6.31
|
||||
|
||||
Dependencies updates
|
||||
|
||||
- orjson v3.10
|
||||
- starlette v0.46.2
|
||||
- schema v0.7.7
|
||||
- pyjwt v2.10.0
|
||||
|
||||
Warning : the on_startup halfAPI argument is now removed, use the lifeSpan
|
||||
|
||||
|
||||
## 0.6.30
|
||||
|
||||
Dependencies updates
|
||||
|
||||
- pyYAML v6.0.1
|
||||
- starlette v0.37.2
|
||||
|
||||
Warning : the on_startup halfAPI argument is now removed, use the lifeSpan
|
||||
|
||||
## 0.6.29
|
||||
|
||||
### Dependencies
|
||||
|
||||
Starlette version bumped to 0.33.
|
||||
|
||||
## 0.6.28
|
||||
|
||||
### Dependencies
|
||||
|
||||
Starlette version bumped to 0.31 (had to disable a test to make it work but
|
||||
seems not important).
|
||||
|
||||
### Development dependencies
|
||||
|
||||
Python 3.7 is no longer supported (openapi_spec_validator is not compatible).
|
||||
|
||||
If you are a developper, you should update dev dependencies in your virtual
|
||||
environment.
|
||||
|
||||
### OpenAPI schemas
|
||||
|
||||
This release improves OpenAPI specification in routes, and gives a default
|
||||
"parameters" field for routes that have path parameters.
|
||||
|
||||
Also, if you use halfAPI for multi-domain setups, you may be annoyed by the
|
||||
change in the return value of the "/" route that differs from "/domain" route.
|
||||
|
||||
An HalfAPI instance should return one and only one OpenAPI Schema, so you can
|
||||
rely on it to connect to other software.
|
||||
|
||||
The version number that is contained under the "info" dictionnary is now the "version"
|
||||
of the Api domain, as specified in the domain dictionnary specified at the root
|
||||
of the Domain.
|
||||
|
||||
The title field of the "info" dictionnary is now the Domain's name.
|
||||
|
||||
The ACLs list is now available under the "info.x-acls" attribute of the schema.
|
||||
It is still accessible from the "/halfapi/acls" route.
|
||||
|
||||
#### Schema Components
|
||||
|
||||
You can now specify a dict in the domain's metadata dict that follows the
|
||||
"components" key of an OpenAPI schema.
|
||||
|
||||
Use it to define models that are used in your API. You can see an exemple of
|
||||
it's use in the "tests/dummy_domain/__init__.py" file.
|
||||
|
||||
|
||||
### ACLs
|
||||
|
||||
The use of an "HEAD" request to check an ACL is now the norm. Please change all
|
||||
the occurrences of your calls on theses routes with the GET method.
|
||||
|
||||
|
||||
### CLI
|
||||
|
||||
Domain command update :
|
||||
|
||||
The `--conftest` flag is now allowed when running the `domain` command, it dumps the current configuration as a TOML string.
|
||||
|
||||
`halfapi domain --conftest my_domain`
|
||||
|
||||
|
||||
The `--dry-run` flag was buggy and is now fixed when using the `domai ` command with the `--run` flag.
|
||||
|
||||
|
||||
### Configuration
|
||||
|
||||
The `port` option in a `domain.my_domain` section in the TOML config file is now prefered to the one in the `project` section.
|
||||
|
||||
The `project` section is used as a default section for the whole configuration file. - Tests still have to be written -
|
||||
|
||||
The standard configuration precedence is fixed, in this order from the hight to the lower :
|
||||
|
||||
- Argument value (i.e. : --log-level)
|
||||
- Environment value (i.e. : HALFAPI_LOGLEVEL)
|
||||
- Configuration value under "domain" key
|
||||
- Configuration value under "project" key
|
||||
- Default configuration value given in the "DEFAULT_CONF" dictionary of halfapi/conf.py
|
||||
|
||||
### Logs
|
||||
|
||||
Small cleanup of the logs levels. If you don't want the config to be dumped, just set the HALFAPI_LOGLEVEL to something different than "DEBUG".
|
||||
|
||||
### Fixes
|
||||
|
||||
- Check an ACL based on a decorator on "/halfapi/acls/MY_ACL"
|
||||
|
||||
## 0.6.27
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- ACLs definition can now include a "public" parameter that defines if there should be an automatic creation of a route to check this acls
|
||||
- /halfapi/acls does not return the "result", it just returns if there is a public route to check the ACL on /halfapi/acls/acl_name
|
||||
=======
|
||||
argument of starlette instead.
|
||||
>>>>>>> a8c59c6 ([release] halfapi 0.6.27)
|
||||
|
||||
## 0.6.26
|
||||
|
||||
- Adds the "base_url", "cookies" and "url" to the "halfapi" argument of route definitions
|
||||
|
||||
## 0.6.25
|
||||
|
||||
- Deletes the "Authorization" cookie on authentication error
|
||||
- Do not raise an exception on signature expiration, use "Nobody" user instead
|
||||
|
||||
## 0.6.24
|
||||
|
||||
- Uses the "Authorization" cookie to read authentication token additionnaly to the "Authorization" header
|
||||
- CLI : allows to run a single domain using the "halfapi domain --run domain_name" command
|
||||
|
||||
## 0.6.23
|
||||
|
||||
Dependency update version
|
||||
|
||||
- starlette v0.23
|
||||
- orjson v3.8.5
|
||||
- click v8
|
||||
- pyJWT v2.6
|
||||
- pyYAML v6
|
||||
- toml v0.10
|
||||
|
||||
## 0.6.22
|
||||
|
||||
- IMPORTANT : Fix bug introduced with 0.6.20 (fix arguments handling)
|
||||
- BREAKING : A domain should now include it's meta datas in a "domain" dictionary located in the __init__.py file of the domain's root. Consider looking in 'tests/dummy_domain/__init__.py'
|
||||
- Add *html* return type as default argument ret_type
|
||||
- Add *txt* return type
|
||||
- Log unhandled exceptions
|
||||
- Log HTTPException with statuscode 500 as critical
|
||||
- PyJWT >=2.4.0,<2.5.0
|
||||
|
||||
|
||||
## 0.6.21
|
||||
|
||||
- Store only domain's config in halfapi['config']
|
||||
- Should run halfapi domain with config_file argument
|
||||
- Testing : You can specify a "MODULE" attribute to point out the path to the Api's base module
|
||||
- Testing : You can specify a "CONFIG" attribute to set the domain's testing configuration
|
||||
- Environment : HALFAPI_DOMAIN_MODULE can be set to specify Api's base module
|
||||
- Config : 'module' attribute can be set to specify Api's base module
|
||||
|
||||
## 0.6.20
|
||||
|
||||
- Fix arguments handling
|
||||
|
||||
## 0.6.19
|
||||
|
||||
- Allow file sending in multipart request (#32)
|
||||
- Add python-multipart dependency
|
||||
|
||||
## 0.6.18
|
||||
|
||||
- Fix config coming from .halfapi/config when using HALFAPI_DOMAIN_NAME environment variable
|
||||
|
||||
## 0.6.17
|
||||
|
||||
- Fix 0.6.16
|
||||
- Errata : HALFAPI_DOMAIN is HALFAPI_DOMAIN_NAME
|
||||
- Testing : You can now specify "MODULE" class attribute for your "test_domain"
|
||||
subclasses
|
||||
|
||||
## 0.6.16
|
||||
|
||||
- The definition of "HALFAPI_DOMAIN_MODULE" environment variable allows to
|
||||
specify the base module for a domain structure. It is formatted as a python
|
||||
import path.
|
||||
The "HALFAPI_DOMAIN" specifies the "name" of the module
|
||||
|
||||
## 0.6.15
|
||||
|
||||
- Allows to define a "__acl__" variable in the API module's __init__.py, to
|
||||
specify how to import the acl lib. You can also specify "acl" in the domain's
|
||||
config
|
||||
|
||||
## 0.6.14
|
||||
|
||||
- Add XLSXResponse (with format argument set to "xlsx"), to return .xlsx files
|
||||
|
||||
## 0.6.13
|
||||
|
||||
- (rollback from 0.6.12) Remove pytest from dependencies in Docker file and
|
||||
remove tests
|
||||
- (dep) Add "packaging" dependency
|
||||
- Add dependency check capability when instantiating a domain (__deps__
|
||||
variable, see in dummy_domain)
|
||||
|
||||
## 0.6.12
|
||||
|
||||
- Installs pytest with dependencies in Docker image, tests when building image
|
||||
|
||||
## 0.6.11
|
||||
|
||||
- Fix "request" has no "path_params" attribute bug
|
||||
|
||||
## 0.6.10
|
||||
|
||||
- Add "x-out" field in HTTP headers when "out" parameters are specified for a
|
||||
route
|
||||
- Add "out" kwarg for not-async functions that specify it
|
||||
|
||||
## 0.6.9
|
||||
|
||||
- Hide data fields in args_check logs
|
||||
|
||||
## 0.6.8
|
||||
|
||||
- Fix testing lib for domains (add default secret and debug option)
|
||||
|
||||
## 0.6.2
|
||||
|
||||
- Domains now need to include the following variables in their __init__.py
|
||||
- __name__ (str, optional)
|
||||
- __id__ (str, optional)
|
||||
- halfapi domain
|
||||
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Mounts domain routers with their ACLs as decorator
|
||||
|
14
Dockerfile
14
Dockerfile
@ -1,11 +1,9 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM python:alpine3.19
|
||||
FROM docker.io/python:3.8.12-slim-bullseye
|
||||
COPY . /halfapi
|
||||
WORKDIR /halfapi
|
||||
ENV VENV_DIR=/opt/venv
|
||||
RUN mkdir -p $VENV_DIR
|
||||
RUN python -m venv $VENV_DIR
|
||||
RUN $VENV_DIR/bin/pip install gunicorn uvicorn
|
||||
RUN $VENV_DIR/bin/pip install .
|
||||
RUN ln -s $VENV_DIR/bin/halfapi /usr/local/bin/
|
||||
CMD $VENV_DIR/bin/gunicorn halfapi.app
|
||||
RUN apt-get update > /dev/null && apt-get -y install git > /dev/null
|
||||
RUN pip install gunicorn uvicorn
|
||||
RUN pip install .
|
||||
CMD gunicorn halfapi.app
|
||||
|
||||
|
27
Pipfile
27
Pipfile
@ -4,33 +4,22 @@ url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
pytest = ">=7,<8"
|
||||
pytest = "*"
|
||||
requests = "*"
|
||||
pytest-asyncio = "*"
|
||||
pylint = "*"
|
||||
build = "*"
|
||||
twine = "*"
|
||||
pyflakes = "*"
|
||||
vulture = "*"
|
||||
virtualenv = "*"
|
||||
httpx = "*"
|
||||
openapi-schema-validator = "*"
|
||||
openapi-spec-validator = "*"
|
||||
coverage = "*"
|
||||
pytest-pythonpath = "*"
|
||||
|
||||
[packages]
|
||||
click = ">=8,<9"
|
||||
starlette = ">=0.46,<0.47"
|
||||
click = ">=7.1,<8"
|
||||
starlette = ">=0.16,<0.17"
|
||||
uvicorn = ">=0.13,<1"
|
||||
orjson = ">=3.10,<4"
|
||||
pyjwt = ">=2.10.0,<2.11.0"
|
||||
pyyaml = ">=6.0.1,<7"
|
||||
orjson = ">=3.4.7,<4"
|
||||
pyjwt = ">=2.0.1,<3"
|
||||
pyyaml = ">=5.3.1,<6"
|
||||
timing-asgi = ">=0.2.1,<1"
|
||||
schema = ">=0.7.7,<1"
|
||||
toml = ">=0.10,<0.11"
|
||||
pip = "*"
|
||||
packaging = ">=19.0"
|
||||
python-multipart = "*"
|
||||
schema = ">=0.7.4,<1"
|
||||
|
||||
[scripts]
|
||||
halfapi = "python -m halfapi"
|
||||
|
1433
Pipfile.lock
generated
1433
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
42
README.md
42
README.md
@ -23,15 +23,17 @@ to reference [HalfORM](https://github.com/collorg/halfORM), a project written by
|
||||
|
||||
Configure HalfAPI in the file : .halfapi/config .
|
||||
|
||||
It's a **toml** file that contains at least two sections, project and domains.
|
||||
|
||||
https://toml.io/en/
|
||||
It's an **ini** file that contains at least two sections, project and domains.
|
||||
|
||||
|
||||
### Project
|
||||
|
||||
The main configuration options without which HalfAPI cannot be run.
|
||||
|
||||
**name** : Project's name
|
||||
|
||||
**halfapi_version** : The HalfAPI version on which you work
|
||||
|
||||
**secret** : The file containing the secret to decode the user's tokens.
|
||||
|
||||
**port** : The port for the test server.
|
||||
@ -41,28 +43,12 @@ The main configuration options without which HalfAPI cannot be run.
|
||||
|
||||
### Domains
|
||||
|
||||
Specify the domains configurations in the following form :
|
||||
The name of the options should be the name of the domains' module, the value is the
|
||||
submodule which contains the routers.
|
||||
|
||||
```
|
||||
[domains.DOMAIN_NAME]
|
||||
name = "DOMAIN_NAME"
|
||||
enabled = true
|
||||
prefix = "/prefix"
|
||||
module = "domain_name.path.to.api.root"
|
||||
port = 1002
|
||||
```
|
||||
Example :
|
||||
|
||||
Specific configuration can be done under the "config" section :
|
||||
|
||||
```
|
||||
[domains.DOMAIN_NAME.config]
|
||||
boolean_option = false
|
||||
string_value = "String"
|
||||
answer = 42
|
||||
listylist = ["hello", "world"]
|
||||
```
|
||||
|
||||
And can be accessed through the app's "config" dictionnary.
|
||||
dummy_domain = .routers
|
||||
|
||||
|
||||
## Usage
|
||||
@ -76,17 +62,9 @@ Run the project by using the `halfapi run` command.
|
||||
You can try the dummy_domain with the following command.
|
||||
|
||||
```
|
||||
PYTHONPATH=$PWD/tests python -m halfapi domain dummy_domain
|
||||
python -m halfapi routes --export --noheader dummy_domain.routers | python -m halfapi run -
|
||||
```
|
||||
|
||||
### CLI documentation
|
||||
|
||||
Use the CLI help.
|
||||
|
||||
```
|
||||
python -m halfapi --help
|
||||
python -m halfapi domain --help
|
||||
```
|
||||
|
||||
## API Testing
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
__version__ = '0.6.31'
|
||||
__version__ = '0.6.0'
|
||||
|
||||
def version():
|
||||
return f'HalfAPI version:{__version__}'
|
||||
|
@ -1,7 +1,9 @@
|
||||
import os
|
||||
from .halfapi import HalfAPI
|
||||
from .conf import CONFIG, SCHEMA
|
||||
from .logging import logger
|
||||
|
||||
def application():
|
||||
from .conf import CONFIG
|
||||
return HalfAPI(CONFIG).application
|
||||
logger.info('CONFIG: %s', CONFIG)
|
||||
logger.info('SCHEMA: %s', SCHEMA)
|
||||
|
||||
application = HalfAPI(
|
||||
CONFIG, SCHEMA or None).application
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
cli/config.py Contains the .halfapi/config
|
||||
cli/config.py Contains the .halfapi/config and HALFAPI_CONF_DIR/project_name templates
|
||||
|
||||
Defines the "halfapi config" command
|
||||
"""
|
||||
@ -9,17 +9,16 @@ import click
|
||||
from .cli import cli
|
||||
from ..conf import CONFIG
|
||||
|
||||
DOMAIN_CONF_STR="""
|
||||
[domain]
|
||||
name = {name}
|
||||
router = {router}
|
||||
"""
|
||||
|
||||
CONF_STR="""
|
||||
[project]
|
||||
name = {project_name}
|
||||
host = {host}
|
||||
port = {port}
|
||||
production = {production}
|
||||
|
||||
[domain]
|
||||
name = {domain_name}
|
||||
router = {router}
|
||||
"""
|
||||
|
||||
|
||||
@ -28,4 +27,4 @@ def config():
|
||||
"""
|
||||
Lists config parameters and their values
|
||||
"""
|
||||
click.echo(CONF_STR.format(**CONFIG))
|
||||
click.echo(CONF_STR)
|
||||
|
@ -8,22 +8,17 @@ import sys
|
||||
import importlib
|
||||
import subprocess
|
||||
|
||||
import json
|
||||
import toml
|
||||
|
||||
import click
|
||||
import orjson
|
||||
import uvicorn
|
||||
|
||||
|
||||
from .cli import cli
|
||||
from ..conf import CONFIG
|
||||
|
||||
from ..half_domain import HalfDomain
|
||||
from ..conf import config, write_config, DOMAINSDICT
|
||||
|
||||
from ..lib.domain import domain_schema
|
||||
from ..lib.schemas import schema_dict_dom
|
||||
from ..lib.routes import api_routes
|
||||
from ..lib.responses import ORJSONResponse
|
||||
from ..conf import CONFIG, PROJECT_LEVEL_KEYS
|
||||
|
||||
|
||||
from ..logging import logger
|
||||
@ -66,7 +61,13 @@ def create_domain(domain_name: str, module_path: str):
|
||||
os.mkdir(router_path)
|
||||
create_init(router_path)
|
||||
|
||||
# TODO: Generate config file
|
||||
|
||||
if not config.has_section('domain'):
|
||||
config.add_section('domain')
|
||||
|
||||
config.set('domain', 'name', domain_name)
|
||||
config.set('domain', 'router', module_path)
|
||||
write_config()
|
||||
|
||||
domain_tree_create()
|
||||
"""
|
||||
@ -112,31 +113,20 @@ def list_routes(domain, m_dom):
|
||||
def list_api_routes():
|
||||
"""
|
||||
Echoes the list of all active domains.
|
||||
|
||||
TODO: Rewrite function
|
||||
"""
|
||||
|
||||
click.echo('# API Routes')
|
||||
# for domain, m_dom in DOMAINSDICT().items():
|
||||
# list_routes(domain, m_dom)
|
||||
for domain, m_dom in DOMAINSDICT().items():
|
||||
list_routes(domain, m_dom)
|
||||
|
||||
|
||||
@click.option('--devel',default=None, is_flag=True)
|
||||
@click.option('--watch',default=False, is_flag=True)
|
||||
@click.option('--production',default=None, is_flag=True)
|
||||
@click.option('--port',default=None, type=int)
|
||||
@click.option('--log-level',default=None, type=str)
|
||||
@click.option('--dry-run',default=False, is_flag=True)
|
||||
@click.option('--run',default=False, is_flag=True)
|
||||
@click.option('--read',default=False, is_flag=True)
|
||||
@click.option('--conftest',default=False, is_flag=True)
|
||||
@click.option('--read',default=True, is_flag=True)
|
||||
@click.option('--create',default=False, is_flag=True)
|
||||
@click.option('--update',default=False, is_flag=True)
|
||||
@click.option('--delete',default=False, is_flag=True)
|
||||
@click.argument('config_file', type=click.File(mode='rb'), required=False)
|
||||
@click.argument('domain',default=None, required=False)
|
||||
@cli.command()
|
||||
def domain(domain, config_file, delete, update, create, conftest, read, run, dry_run, log_level, port, production, watch, devel):
|
||||
def domain(domain, delete, update, create, read): #, domains, read, create, update, delete):
|
||||
"""
|
||||
The "halfapi domain" command
|
||||
|
||||
@ -153,90 +143,16 @@ def domain(domain, config_file, delete, update, create, conftest, read, run, dry
|
||||
# TODO: Connect to the create_domain function
|
||||
raise NotImplementedError
|
||||
raise Exception('Missing domain name')
|
||||
|
||||
if config_file:
|
||||
ARG_CONFIG = toml.load(config_file.name)
|
||||
|
||||
if 'project' in ARG_CONFIG:
|
||||
for key, value in ARG_CONFIG['project'].items():
|
||||
if key in PROJECT_LEVEL_KEYS:
|
||||
CONFIG[key] = value
|
||||
|
||||
if 'domain' in ARG_CONFIG and domain in ARG_CONFIG['domain']:
|
||||
for key, value in ARG_CONFIG['domain'][domain].items():
|
||||
if key in PROJECT_LEVEL_KEYS:
|
||||
CONFIG[key] = value
|
||||
|
||||
CONFIG['domain'].update(ARG_CONFIG['domain'])
|
||||
|
||||
if create:
|
||||
if update:
|
||||
raise NotImplementedError
|
||||
elif update:
|
||||
if delete:
|
||||
raise NotImplementedError
|
||||
elif delete:
|
||||
raise NotImplementedError
|
||||
elif read:
|
||||
from ..halfapi import HalfAPI
|
||||
if read:
|
||||
m_domain = importlib.import_module(domain)
|
||||
|
||||
halfapi = HalfAPI(CONFIG)
|
||||
click.echo(orjson.dumps(
|
||||
halfapi.domains[domain].schema(),
|
||||
domain_schema(m_domain),
|
||||
option=orjson.OPT_NON_STR_KEYS,
|
||||
default=ORJSONResponse.default_cast)
|
||||
)
|
||||
|
||||
else:
|
||||
if dry_run:
|
||||
CONFIG['dryrun'] = True
|
||||
|
||||
domains = CONFIG.get('domain')
|
||||
for key in domains.keys():
|
||||
if key != domain:
|
||||
domains[key]['enabled'] = False
|
||||
else:
|
||||
domains[key]['enabled'] = True
|
||||
|
||||
if not log_level:
|
||||
log_level = CONFIG.get('domain', {}).get('loglevel', CONFIG.get('loglevel', False))
|
||||
else:
|
||||
CONFIG['loglevel'] = log_level
|
||||
|
||||
if not port:
|
||||
port = CONFIG.get('domain', {}).get('port', CONFIG.get('port', False))
|
||||
else:
|
||||
CONFIG['port'] = port
|
||||
|
||||
if devel is None and production is not None and (production is False or production is True):
|
||||
CONFIG['production'] = production
|
||||
|
||||
if devel is not None:
|
||||
CONFIG['production'] = False
|
||||
CONFIG['loglevel'] = 'debug'
|
||||
|
||||
|
||||
if conftest:
|
||||
click.echo(
|
||||
toml.dumps(CONFIG)
|
||||
)
|
||||
|
||||
else:
|
||||
# domain section port is preferred, if it doesn't exist we use the global one
|
||||
|
||||
uvicorn_kwargs = {}
|
||||
|
||||
if CONFIG.get('port'):
|
||||
uvicorn_kwargs['port'] = CONFIG['port']
|
||||
|
||||
if CONFIG.get('loglevel'):
|
||||
uvicorn_kwargs['log_level'] = CONFIG['loglevel'].lower()
|
||||
|
||||
if watch:
|
||||
uvicorn_kwargs['reload'] = True
|
||||
|
||||
uvicorn.run(
|
||||
'halfapi.app:application',
|
||||
factory=True,
|
||||
**uvicorn_kwargs
|
||||
)
|
||||
|
||||
sys.exit(0)
|
||||
|
@ -19,6 +19,7 @@ from .cli import cli
|
||||
from ..logging import logger
|
||||
|
||||
TMPL_HALFAPI_ETC = """[project]
|
||||
name = {project}
|
||||
host = 127.0.0.1
|
||||
port = 8000
|
||||
secret = /path/to/secret_file
|
||||
@ -26,7 +27,17 @@ production = False
|
||||
base_dir = {base_dir}
|
||||
"""
|
||||
|
||||
def format_halfapi_etc(project, path):
|
||||
"""
|
||||
Returns the formatted template for /etc/half_api/PROJECT_NAME
|
||||
"""
|
||||
return TMPL_HALFAPI_ETC.format(
|
||||
project=project,
|
||||
base_dir=path
|
||||
)
|
||||
|
||||
TMPL_HALFAPI_CONFIG = """[project]
|
||||
name = {name}
|
||||
halfapi_version = {halfapi_version}
|
||||
|
||||
[domain]
|
||||
@ -55,7 +66,9 @@ def init(project):
|
||||
|
||||
with open(f'{project}/.halfapi/config', 'w') as conf_file:
|
||||
conf_file.write(TMPL_HALFAPI_CONFIG.format(
|
||||
name=project,
|
||||
halfapi_version=__version__))
|
||||
|
||||
|
||||
click.echo(f'Configure halfapi project in {CONF_DIR}/{project}')
|
||||
click.echo(format_halfapi_etc(project, CONF_DIR))
|
||||
|
@ -14,9 +14,11 @@ from .cli import cli
|
||||
|
||||
from ..logging import logger
|
||||
|
||||
# from ..lib.domain import domain_schema_dict
|
||||
from ..lib.domain import domain_schema_dict
|
||||
from ..lib.constants import DOMAIN_SCHEMA, ROUTE_SCHEMA
|
||||
from ..lib.responses import ORJSONResponse
|
||||
# from ..lib.routes import api_routes
|
||||
from ..lib.schemas import schema_to_csv # get_api_routes
|
||||
|
||||
@click.argument('module', required=True)
|
||||
@click.option('--export', default=False, is_flag=True)
|
||||
@ -29,26 +31,26 @@ def routes(module, export=False, validate=False, check=False, noheader=False, sc
|
||||
"""
|
||||
The "halfapi routes" command
|
||||
"""
|
||||
# try:
|
||||
# mod = importlib.import_module(module)
|
||||
# except ImportError as exc:
|
||||
# raise click.BadParameter('Cannot import this module', param=module) from exc
|
||||
try:
|
||||
mod = importlib.import_module(module)
|
||||
except ImportError as exc:
|
||||
raise click.BadParameter('Cannot import this module', param=module) from exc
|
||||
|
||||
# if export:
|
||||
# click.echo(schema_to_csv(module, header=not noheader))
|
||||
if export:
|
||||
click.echo(schema_to_csv(module, header=not noheader))
|
||||
|
||||
# if schema:
|
||||
# routes_d = domain_schema_dict(mod)
|
||||
# ROUTE_SCHEMA.validate(routes_d)
|
||||
# click.echo(orjson.dumps(routes_d,
|
||||
# option=orjson.OPT_NON_STR_KEYS,
|
||||
# default=ORJSONResponse.default_cast))
|
||||
if schema:
|
||||
routes_d = domain_schema_dict(mod)
|
||||
ROUTE_SCHEMA.validate(routes_d)
|
||||
click.echo(orjson.dumps(routes_d,
|
||||
option=orjson.OPT_NON_STR_KEYS,
|
||||
default=ORJSONResponse.default_cast))
|
||||
|
||||
|
||||
# if validate:
|
||||
# routes_d = domain_schema_dict(mod)
|
||||
# try:
|
||||
# ROUTE_SCHEMA.validate(routes_d)
|
||||
# except Exception as exc:
|
||||
# raise exc
|
||||
if validate:
|
||||
routes_d = domain_schema_dict(mod)
|
||||
try:
|
||||
ROUTE_SCHEMA.validate(routes_d)
|
||||
except Exception as exc:
|
||||
raise exc
|
||||
|
||||
|
@ -9,25 +9,24 @@ import uvicorn
|
||||
|
||||
from .cli import cli
|
||||
from .domain import list_api_routes
|
||||
from ..conf import CONFIG, SCHEMA
|
||||
from ..conf import (PROJECT_NAME, HOST, PORT, SCHEMA,
|
||||
PRODUCTION, LOGLEVEL, DOMAINSDICT, CONFIG, DOMAIN, ROUTER)
|
||||
from ..logging import logger
|
||||
from ..lib.schemas import schema_csv_dict
|
||||
from ..half_domain import HalfDomain
|
||||
|
||||
@click.option('--host', default=CONFIG.get('host'))
|
||||
@click.option('--port', default=CONFIG.get('port'))
|
||||
@click.option('--host', default=HOST)
|
||||
@click.option('--port', default=PORT)
|
||||
@click.option('--reload', default=False)
|
||||
@click.option('--secret', default=CONFIG.get('secret'))
|
||||
@click.option('--production', default=CONFIG.get('secret'))
|
||||
@click.option('--loglevel', default=CONFIG.get('loglevel'))
|
||||
@click.option('--secret', default=False)
|
||||
@click.option('--production', default=True)
|
||||
@click.option('--loglevel', default=LOGLEVEL)
|
||||
@click.option('--prefix', default='/')
|
||||
@click.option('--check', default=True)
|
||||
@click.option('--dryrun', default=False, is_flag=True)
|
||||
@click.argument('schema', type=click.File('r'), required=False)
|
||||
@click.argument('router', required=False)
|
||||
@click.argument('domain', required=False)
|
||||
@cli.command()
|
||||
def run(host, port, reload, secret, production, loglevel, prefix, check, dryrun,
|
||||
schema, domain):
|
||||
def run(host, port, reload, secret, production, loglevel, prefix, check, schema, router, domain):
|
||||
"""
|
||||
The "halfapi run" command
|
||||
"""
|
||||
@ -35,53 +34,41 @@ def run(host, port, reload, secret, production, loglevel, prefix, check, dryrun,
|
||||
host, port, reload, secret, production, loglevel, prefix, schema
|
||||
)
|
||||
|
||||
if not host:
|
||||
host = HOST
|
||||
|
||||
if not port:
|
||||
port = PORT
|
||||
|
||||
port = int(port)
|
||||
|
||||
if production and reload:
|
||||
if PRODUCTION and reload:
|
||||
reload = False
|
||||
raise Exception('Can\'t use live code reload in production')
|
||||
|
||||
click.echo(f'Launching application')
|
||||
log_level = LOGLEVEL or 'info'
|
||||
|
||||
if secret:
|
||||
CONFIG['secret'] = secret
|
||||
click.echo(f'Launching application {PROJECT_NAME}')
|
||||
|
||||
CONFIG.get('domain')['name'] = domain
|
||||
CONFIG.get('domain')['router'] = router
|
||||
|
||||
if schema:
|
||||
# Populate the SCHEMA global with the data from the given file
|
||||
for key, val in schema_csv_dict(schema, prefix).items():
|
||||
SCHEMA[key] = val
|
||||
|
||||
if domain:
|
||||
# If we specify a domain to run as argument
|
||||
if 'domain' not in CONFIG:
|
||||
CONFIG['domain'] = {}
|
||||
|
||||
# Disable all domains
|
||||
keys = list(CONFIG.get('domain').keys())
|
||||
for key in keys:
|
||||
CONFIG['domain'].pop(key)
|
||||
|
||||
# And activate the desired one, mounted without prefix
|
||||
CONFIG['domain'][domain] = {
|
||||
'name': domain,
|
||||
'prefix': False,
|
||||
'enabled': True
|
||||
}
|
||||
|
||||
# list_api_routes()
|
||||
|
||||
click.echo(f'uvicorn.run("halfapi.app:application"\n' \
|
||||
f'host: {host}\n' \
|
||||
f'port: {port}\n' \
|
||||
f'log_level: {loglevel}\n' \
|
||||
f'log_level: {log_level}\n' \
|
||||
f'reload: {reload}\n'
|
||||
)
|
||||
|
||||
if dryrun:
|
||||
CONFIG['dryrun'] = True
|
||||
|
||||
uvicorn.run('halfapi.app:application',
|
||||
host=host,
|
||||
port=int(port),
|
||||
log_level=loglevel,
|
||||
log_level=log_level,
|
||||
reload=reload)
|
||||
|
236
halfapi/conf.py
236
halfapi/conf.py
@ -10,13 +10,14 @@ It uses the following environment variables :
|
||||
It defines the following globals :
|
||||
|
||||
- PROJECT_NAME (str) - HALFAPI_PROJECT_NAME
|
||||
- DOMAINSDICT ({domain_name: domain_module}) - HALFAPI_DOMAIN_NAME / HALFAPI_DOMAIN_MODULE
|
||||
- PRODUCTION (bool) - HALFAPI_PRODUCTION
|
||||
- LOGLEVEL (str) - HALFAPI_LOGLEVEL
|
||||
- LOGLEVEL (string) - HALFAPI_LOGLEVEL
|
||||
- BASE_DIR (str) - HALFAPI_BASE_DIR
|
||||
- HOST (str) - HALFAPI_HOST
|
||||
- PORT (int) - HALFAPI_PORT
|
||||
- CONF_DIR (str) - HALFAPI_CONF_DIR
|
||||
- DRYRUN (bool) - HALFAPI_DRYRUN
|
||||
- config (ConfigParser)
|
||||
|
||||
It reads the following ressource :
|
||||
|
||||
@ -25,181 +26,132 @@ It reads the following ressource :
|
||||
It follows the following format :
|
||||
|
||||
[project]
|
||||
name = PROJECT_NAME
|
||||
halfapi_version = HALFAPI_VERSION
|
||||
|
||||
[domain.domain_name]
|
||||
name = domain_name
|
||||
routers = routers
|
||||
|
||||
[domain.domain_name.config]
|
||||
option = Argh
|
||||
|
||||
[domains]
|
||||
domain_name = requirements-like-url
|
||||
"""
|
||||
|
||||
from .logging import logger
|
||||
import logging
|
||||
import os
|
||||
from os import environ
|
||||
import sys
|
||||
from configparser import ConfigParser
|
||||
import importlib
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
import toml
|
||||
from .lib.domain import d_domains
|
||||
from .logging import logger
|
||||
|
||||
|
||||
PROJECT_NAME = environ.get('HALFAPI_PROJECT_NAME') or os.path.basename(os.getcwd())
|
||||
DOMAINSDICT = lambda: {}
|
||||
DOMAINS = {}
|
||||
PRODUCTION = True
|
||||
LOGLEVEL = 'info'
|
||||
HOST = '127.0.0.1'
|
||||
PORT = '3000'
|
||||
SECRET = ''
|
||||
CONF_FILE = os.environ.get('HALFAPI_CONF_FILE', '.halfapi/config')
|
||||
|
||||
DOMAIN = None
|
||||
ROUTER = None
|
||||
SCHEMA = {}
|
||||
|
||||
DEFAULT_CONF = {
|
||||
# Default configuration values
|
||||
'SECRET': tempfile.mkstemp()[1],
|
||||
'PROJECT_NAME': os.getcwd().split('/')[-1],
|
||||
'PRODUCTION': True,
|
||||
'HOST': '127.0.0.1',
|
||||
'PORT': 3000,
|
||||
'LOGLEVEL': 'info',
|
||||
'BASE_DIR': os.getcwd(),
|
||||
'CONF_FILE': '.halfapi/config',
|
||||
'CONF_DIR': '/etc/half_api',
|
||||
'DRYRUN': None
|
||||
}
|
||||
|
||||
PROJECT_LEVEL_KEYS = {
|
||||
# Allowed keys in "project" section of configuration file
|
||||
'project_name',
|
||||
'production',
|
||||
'secret',
|
||||
'host',
|
||||
'port',
|
||||
'loglevel',
|
||||
'dryrun'
|
||||
}
|
||||
|
||||
DOMAIN_LEVEL_KEYS = PROJECT_LEVEL_KEYS | {
|
||||
# Allowed keys in "domain" section of configuration file
|
||||
'name',
|
||||
'module',
|
||||
'prefix',
|
||||
'enabled'
|
||||
}
|
||||
|
||||
CONF_FILE = os.environ.get('HALFAPI_CONF_FILE', DEFAULT_CONF['CONF_FILE'])
|
||||
CONF_DIR = os.environ.get('HALFAPI_CONF_DIR', DEFAULT_CONF['CONF_DIR'])
|
||||
config = ConfigParser(allow_no_value=True)
|
||||
|
||||
CONF_DIR = environ.get('HALFAPI_CONF_DIR', '/etc/half_api')
|
||||
HALFAPI_ETC_FILE=os.path.join(
|
||||
CONF_DIR, 'config'
|
||||
CONF_DIR, 'default.ini'
|
||||
)
|
||||
|
||||
BASE_DIR = os.environ.get('HALFAPI_BASE_DIR', DEFAULT_CONF['BASE_DIR'])
|
||||
HALFAPI_DOT_FILE=os.path.join(
|
||||
BASE_DIR, '.halfapi', 'config')
|
||||
os.getcwd(), '.halfapi', 'config')
|
||||
|
||||
HALFAPI_CONFIG_FILES = []
|
||||
HALFAPI_CONFIG_FILES = [ CONF_FILE, HALFAPI_DOT_FILE ]
|
||||
|
||||
try:
|
||||
with open(HALFAPI_ETC_FILE, 'r'):
|
||||
HALFAPI_CONFIG_FILES.append(HALFAPI_ETC_FILE)
|
||||
except FileNotFoundError:
|
||||
logger.info('Cannot find a configuration file under %s', HALFAPI_ETC_FILE)
|
||||
|
||||
try:
|
||||
with open(HALFAPI_DOT_FILE, 'r'):
|
||||
HALFAPI_CONFIG_FILES.append(HALFAPI_DOT_FILE)
|
||||
except FileNotFoundError:
|
||||
logger.info('Cannot find a configuration file under %s', HALFAPI_DOT_FILE)
|
||||
def conf_files():
|
||||
return [
|
||||
os.path.join(
|
||||
CONF_DIR, 'default.ini'
|
||||
),
|
||||
os.path.join(
|
||||
os.getcwd(), '.halfapi', 'config')]
|
||||
|
||||
|
||||
ENVIRONMENT = {}
|
||||
# Load environment variables allowed in configuration
|
||||
def write_config():
|
||||
"""
|
||||
Writes the current config to the highest priority config file
|
||||
"""
|
||||
with open(conf_files()[-1], 'w') as halfapi_config:
|
||||
config.write(halfapi_config)
|
||||
|
||||
if 'HALFAPI_DRYRUN' in os.environ:
|
||||
ENVIRONMENT['dryrun'] = True
|
||||
def config_dict():
|
||||
"""
|
||||
The config object as a dict
|
||||
"""
|
||||
return {
|
||||
section: dict(config.items(section))
|
||||
for section in config.sections()
|
||||
}
|
||||
|
||||
if 'HALFAPI_PROD' in os.environ:
|
||||
ENVIRONMENT['production'] = bool(os.environ.get('HALFAPI_PROD'))
|
||||
|
||||
if 'HALFAPI_LOGLEVEL' in os.environ:
|
||||
ENVIRONMENT['loglevel'] = os.environ.get('HALFAPI_LOGLEVEL').lower()
|
||||
|
||||
if 'HALFAPI_SECRET' in os.environ:
|
||||
ENVIRONMENT['secret'] = os.environ.get('HALFAPI_SECRET')
|
||||
|
||||
if 'HALFAPI_HOST' in os.environ:
|
||||
ENVIRONMENT['host'] = os.environ.get('HALFAPI_HOST')
|
||||
|
||||
if 'HALFAPI_PORT' in os.environ:
|
||||
ENVIRONMENT['port'] = int(os.environ.get('HALFAPI_PORT'))
|
||||
|
||||
def read_config(filenames=HALFAPI_CONFIG_FILES):
|
||||
def read_config():
|
||||
"""
|
||||
The highest index in "filenames" are the highest priorty
|
||||
"""
|
||||
d_res = {}
|
||||
config.read(HALFAPI_CONFIG_FILES)
|
||||
|
||||
logger.info('Reading config files %s', filenames)
|
||||
for CONF_FILE in filenames:
|
||||
if os.path.isfile(CONF_FILE):
|
||||
conf_dict = toml.load(CONF_FILE)
|
||||
d_res.update(conf_dict)
|
||||
|
||||
logger.info('Read config files (result) %s', d_res)
|
||||
return { **d_res.get('project', {}), 'domain': d_res.get('domain', {}) }
|
||||
|
||||
CONFIG = read_config()
|
||||
CONFIG.update(**ENVIRONMENT)
|
||||
CONFIG = {}
|
||||
read_config()
|
||||
|
||||
PROJECT_NAME = CONFIG.get('project_name',
|
||||
os.environ.get('HALFAPI_PROJECT_NAME', DEFAULT_CONF['PROJECT_NAME']))
|
||||
PROJECT_NAME = config.get('project', 'name', fallback=PROJECT_NAME)
|
||||
|
||||
if os.environ.get('HALFAPI_DOMAIN_NAME'):
|
||||
# Force enabled domain by environment variable
|
||||
if len(PROJECT_NAME) == 0:
|
||||
raise Exception('Need a project name as argument')
|
||||
|
||||
DOMAIN_NAME = os.environ.get('HALFAPI_DOMAIN_NAME')
|
||||
if 'domain' in CONFIG and DOMAIN_NAME in CONFIG['domain'] \
|
||||
and 'config' in CONFIG['domain'][DOMAIN_NAME]:
|
||||
DOMAINSDICT = lambda: d_domains(config)
|
||||
DOMAINS = DOMAINSDICT()
|
||||
if len(DOMAINS) == 0:
|
||||
logger.info('Running without domains: %s', d_domains(config) or 'empty domain dictionary')
|
||||
|
||||
domain_config = CONFIG['domain'][DOMAIN_NAME]['config']
|
||||
else:
|
||||
domain_config = {}
|
||||
HOST = config.get('project', 'host', fallback=environ.get('HALFAPI_HOST', '127.0.0.1'))
|
||||
PORT = config.getint('project', 'port', fallback=environ.get('HALFAPI_PORT', '3000'))
|
||||
|
||||
CONFIG['domain'] = {}
|
||||
secret_path = config.get('project', 'secret', fallback=environ.get('HALFAPI_SECRET', ''))
|
||||
try:
|
||||
with open(secret_path, 'r') as secret_file:
|
||||
|
||||
CONFIG['domain'][DOMAIN_NAME] = {
|
||||
'enabled': True,
|
||||
'name': DOMAIN_NAME,
|
||||
'prefix': False
|
||||
SECRET = secret_file.read().strip()
|
||||
CONFIG['secret'] = SECRET.strip()
|
||||
except FileNotFoundError as exc:
|
||||
logger.info('Running without secret file: %s', secret_path or 'no file specified')
|
||||
|
||||
PRODUCTION = config.getboolean('project', 'production',
|
||||
fallback=environ.get('HALFAPI_PROD', True))
|
||||
|
||||
LOGLEVEL = config.get('project', 'loglevel',
|
||||
fallback=environ.get('HALFAPI_LOGLEVEL', 'info')).lower()
|
||||
|
||||
BASE_DIR = config.get('project', 'base_dir',
|
||||
fallback=environ.get('HALFAPI_BASE_DIR', '.'))
|
||||
|
||||
CONFIG = {
|
||||
'project_name': PROJECT_NAME,
|
||||
'production': PRODUCTION,
|
||||
'secret': SECRET,
|
||||
'host': HOST,
|
||||
'port': PORT,
|
||||
'domain': {
|
||||
'name': None,
|
||||
'router': None
|
||||
},
|
||||
'domain_config': {}
|
||||
}
|
||||
|
||||
CONFIG['domain'][DOMAIN_NAME]['config'] = domain_config
|
||||
for domain in DOMAINS:
|
||||
if domain not in config.sections():
|
||||
continue
|
||||
|
||||
if os.environ.get('HALFAPI_DOMAIN_MODULE'):
|
||||
# Specify the pythonpath to import the specified domain (defaults to global)
|
||||
dom_module = os.environ.get('HALFAPI_DOMAIN_MODULE')
|
||||
CONFIG['domain'][DOMAIN_NAME]['module'] = dom_module
|
||||
|
||||
if len(CONFIG.get('domain', {}).keys()) == 0:
|
||||
logger.info('No domains')
|
||||
|
||||
|
||||
# Secret
|
||||
if 'secret' not in CONFIG:
|
||||
# TODO: Create a temporary secret
|
||||
CONFIG['secret'] = DEFAULT_CONF['SECRET']
|
||||
with open(CONFIG['secret'], 'w') as secret_file:
|
||||
secret_file.write(str(uuid.uuid4()))
|
||||
|
||||
try:
|
||||
with open(CONFIG['secret'], 'r') as secret_file:
|
||||
CONFIG['secret'] = CONFIG['secret'].strip()
|
||||
except FileNotFoundError as exc:
|
||||
logger.warning('Running without secret file: %s', CONFIG['secret'] or 'no file specified')
|
||||
|
||||
CONFIG.setdefault('project_name', DEFAULT_CONF['PROJECT_NAME'])
|
||||
CONFIG.setdefault('production', DEFAULT_CONF['PRODUCTION'])
|
||||
CONFIG.setdefault('host', DEFAULT_CONF['HOST'])
|
||||
CONFIG.setdefault('port', DEFAULT_CONF['PORT'])
|
||||
CONFIG.setdefault('loglevel', DEFAULT_CONF['LOGLEVEL'])
|
||||
CONFIG.setdefault('dryrun', DEFAULT_CONF['DRYRUN'])
|
||||
|
||||
# !!!TO REMOVE!!!
|
||||
SECRET = CONFIG.get('secret')
|
||||
PRODUCTION = CONFIG.get('production')
|
||||
# !!!
|
||||
CONFIG['domain_config'][domain] = dict(config.items(domain))
|
||||
|
@ -1,513 +0,0 @@
|
||||
import importlib
|
||||
import inspect
|
||||
import os
|
||||
import re
|
||||
|
||||
from packaging.specifiers import SpecifierSet
|
||||
from packaging.version import Version
|
||||
from typing import Coroutine, Dict, Iterator, List, Tuple
|
||||
from types import ModuleType, FunctionType
|
||||
|
||||
from schema import SchemaError
|
||||
|
||||
from starlette.applications import Starlette
|
||||
from starlette.middleware import Middleware
|
||||
from starlette.routing import Router, Route
|
||||
from starlette.schemas import SchemaGenerator
|
||||
|
||||
from .lib.acl import AclRoute
|
||||
from .lib.responses import ORJSONResponse
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
from . import __version__
|
||||
from .lib.constants import API_SCHEMA_DICT, ROUTER_SCHEMA, VERBS
|
||||
from .half_route import HalfRoute
|
||||
from .lib import acl as lib_acl
|
||||
from .lib.responses import PlainTextResponse
|
||||
from .lib.routes import JSONRoute
|
||||
from .lib.schemas import param_docstring_default
|
||||
from .lib.domain import MissingAclError, PathError, UnknownPathParameterType, \
|
||||
UndefinedRoute, UndefinedFunction, get_fct_name, route_decorator
|
||||
from .lib.domain_middleware import DomainMiddleware
|
||||
from .logging import logger
|
||||
|
||||
class HalfDomain(Starlette):
|
||||
def __init__(self, domain, module=None, router=None, acl=None, app=None):
|
||||
"""
|
||||
Parameters:
|
||||
domain (str): Module name (should be importable)
|
||||
router (str): Router name (should be importable from domain module
|
||||
defaults to __router__ variable from domain module)
|
||||
app (HalfAPI): The app instance
|
||||
"""
|
||||
self.app = app
|
||||
|
||||
self.m_domain = importlib.import_module(domain) if module is None else module
|
||||
self.d_domain = getattr(self.m_domain, 'domain', domain)
|
||||
self.name = self.d_domain['name']
|
||||
self.id = self.d_domain['id']
|
||||
self.version = self.d_domain['version']
|
||||
self.halfapi_version = self.d_domain.get('halfapi_version', __version__)
|
||||
self.deps = self.d_domain.get('deps', tuple())
|
||||
self.schema_components = self.d_domain.get('schema_components', dict())
|
||||
|
||||
if not router:
|
||||
self.router = self.d_domain.get('routers', '.routers')
|
||||
else:
|
||||
self.router = router
|
||||
|
||||
self.m_router = None
|
||||
try:
|
||||
self.m_router = importlib.import_module(self.router, self.m_domain.__package__)
|
||||
except AttributeError:
|
||||
raise Exception('no router module')
|
||||
|
||||
self.m_acl = HalfDomain.m_acl(self.m_domain, acl)
|
||||
|
||||
self.config = { **app.config }
|
||||
|
||||
logger.info('HalfDomain creation %s %s', domain, self.config)
|
||||
|
||||
for elt in self.deps:
|
||||
package, version = elt
|
||||
specifier = SpecifierSet(version)
|
||||
package_module = importlib.import_module(package)
|
||||
if Version(package_module.__version__) not in specifier:
|
||||
raise Exception(
|
||||
'Wrong version for package {} version {} (excepting {})'.format(
|
||||
package, package_module.__version__, specifier
|
||||
))
|
||||
|
||||
|
||||
super().__init__(
|
||||
routes=self.gen_domain_routes(),
|
||||
middleware=[
|
||||
Middleware(
|
||||
DomainMiddleware,
|
||||
domain={
|
||||
'name': self.name,
|
||||
'id': self.id,
|
||||
'version': self.version,
|
||||
'halfapi_version': self.halfapi_version,
|
||||
'config': self.config.get('domain', {}).get(self.name, {}).get('config', {})
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def name(module):
|
||||
""" Returns the name declared in the 'domain' dict at the root of the package
|
||||
"""
|
||||
return module.domain['name']
|
||||
|
||||
|
||||
@staticmethod
|
||||
def m_acl(module, acl=None):
|
||||
""" Returns the imported acl module for the domain module
|
||||
"""
|
||||
if not acl:
|
||||
acl = getattr(module, '__acl__', '.acl')
|
||||
|
||||
return importlib.import_module(acl, module.__package__)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def acls(module, acl=None):
|
||||
""" Returns the ACLS constant for the given domain
|
||||
"""
|
||||
m_acl = HalfDomain.m_acl(module, acl)
|
||||
try:
|
||||
return [
|
||||
lib_acl.ACL(*elt)
|
||||
for elt in getattr(m_acl, 'ACLS')
|
||||
]
|
||||
except AttributeError as exc:
|
||||
logger.error(exc)
|
||||
raise Exception(
|
||||
f'Missing acl.ACLS constant in module {m_acl.__package__}') from exc
|
||||
|
||||
@staticmethod
|
||||
def acls_route(domain, module_path=None, acl=None):
|
||||
""" Dictionary of acls
|
||||
|
||||
Format :
|
||||
|
||||
{
|
||||
[acl_name]: {
|
||||
callable: fct_reference,
|
||||
docs: fct_docstring,
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
d_res = {}
|
||||
|
||||
module = importlib.import_module(domain) \
|
||||
if module_path is None \
|
||||
else importlib.import_module(module_path)
|
||||
|
||||
m_acl = HalfDomain.m_acl(module, acl)
|
||||
|
||||
for elt in HalfDomain.acls(module, acl=acl):
|
||||
|
||||
fct = getattr(m_acl, elt.name)
|
||||
|
||||
d_res[elt.name] = {
|
||||
'callable': fct,
|
||||
'docs': elt.documentation
|
||||
}
|
||||
|
||||
return d_res
|
||||
|
||||
@staticmethod
|
||||
def acls_router(domain, module_path=None, acl=None):
|
||||
""" Returns a Router object with the following routes :
|
||||
|
||||
/ : The "acls" field of the API metadatas
|
||||
/{acl_name} : If the ACL is defined as public, a route that returns either status code 200 or 401 on HEAD/GET request
|
||||
"""
|
||||
|
||||
routes = []
|
||||
d_res = {}
|
||||
|
||||
module = importlib.import_module(domain) \
|
||||
if module_path is None \
|
||||
else importlib.import_module(module_path)
|
||||
|
||||
|
||||
m_acl = HalfDomain.m_acl(module, acl)
|
||||
|
||||
for elt in HalfDomain.acls(module, acl=acl):
|
||||
|
||||
fct = getattr(m_acl, elt.name)
|
||||
|
||||
d_res[elt.name] = {
|
||||
'callable': fct,
|
||||
'docs': elt.documentation,
|
||||
'public': elt.public
|
||||
}
|
||||
|
||||
if elt.public:
|
||||
try:
|
||||
if inspect.iscoroutinefunction(fct):
|
||||
logger.warning('async decorator are not yet supported')
|
||||
else:
|
||||
inner = fct()
|
||||
|
||||
if inspect.iscoroutinefunction(fct) or callable(inner):
|
||||
fct = inner
|
||||
|
||||
except TypeError:
|
||||
# Fct is not a decorator or is not well called (has no default arguments)
|
||||
# We can ignore this
|
||||
pass
|
||||
|
||||
routes.append(
|
||||
AclRoute(f'/{elt.name}', fct, elt)
|
||||
)
|
||||
|
||||
d_res_under_domain_name = {}
|
||||
d_res_under_domain_name[HalfDomain.name(module)] = d_res
|
||||
|
||||
routes.append(
|
||||
Route(
|
||||
'/',
|
||||
JSONRoute(d_res_under_domain_name),
|
||||
methods=['GET']
|
||||
)
|
||||
)
|
||||
|
||||
return Router(routes)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def gen_routes(m_router: ModuleType,
|
||||
verb: str,
|
||||
path: List[str],
|
||||
params: List[Dict],
|
||||
path_param_docstrings: Dict[str, str] = {}) -> Tuple[FunctionType, Dict]:
|
||||
"""
|
||||
Returns a tuple of the function associatied to the verb and path arguments,
|
||||
and the dictionary of it's acls
|
||||
|
||||
Parameters:
|
||||
- m_router (ModuleType): The module containing the function definition
|
||||
|
||||
- verb (str): The HTTP verb for the route (GET, POST, ...)
|
||||
|
||||
- path (List): The route path, as a list (each item being a level of
|
||||
deepness), from the lowest level (domain) to the highest
|
||||
|
||||
- params (Dict): The acl list of the following format :
|
||||
[{'acl': Function, 'args': {'required': [], 'optional': []}}]
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
(Function, Dict): The destination function and the acl dictionary
|
||||
|
||||
"""
|
||||
if len(params) == 0:
|
||||
raise MissingAclError('[{}] {}'.format(verb, '/'.join(path)))
|
||||
|
||||
if len(path) == 0:
|
||||
logger.error('Empty path for [{%s}]', verb)
|
||||
raise PathError()
|
||||
|
||||
fct_name = get_fct_name(verb, path[-1])
|
||||
if hasattr(m_router, fct_name):
|
||||
fct = getattr(m_router, fct_name)
|
||||
fct_docstring_obj = yaml.safe_load(fct.__doc__)
|
||||
if 'parameters' not in fct_docstring_obj and path_param_docstrings:
|
||||
fct_docstring_obj['parameters'] = list(map(
|
||||
yaml.safe_load,
|
||||
path_param_docstrings.values()))
|
||||
|
||||
fct.__doc__ = yaml.dump(fct_docstring_obj)
|
||||
else:
|
||||
raise UndefinedFunction('{}.{}'.format(m_router.__name__, fct_name or ''))
|
||||
|
||||
|
||||
if not inspect.iscoroutinefunction(fct):
|
||||
return route_decorator(fct), params
|
||||
|
||||
# TODO: Remove when using only sync functions
|
||||
return lib_acl.args_check(fct), params
|
||||
|
||||
|
||||
@staticmethod
|
||||
def gen_router_routes(m_router, path: List[str], PATH_PARAMS={}) -> \
|
||||
Iterator[Tuple[str, str, ModuleType, Coroutine, List]]:
|
||||
"""
|
||||
Recursive generator that parses a router (or a subrouter)
|
||||
and yields from gen_routes
|
||||
|
||||
Parameters:
|
||||
|
||||
- m_router (ModuleType): The currently treated router module
|
||||
- path (List[str]): The current path stack
|
||||
|
||||
Yields:
|
||||
|
||||
(str, str, ModuleType, Coroutine, List): A tuple containing the path, verb,
|
||||
router module, function reference and parameters of the route.
|
||||
Function and parameters are yielded from then gen_routes function,
|
||||
that decorates the endpoint function.
|
||||
"""
|
||||
|
||||
for subpath, params in HalfDomain.read_router(m_router).items():
|
||||
path.append(subpath)
|
||||
|
||||
for verb in VERBS:
|
||||
if verb not in params:
|
||||
continue
|
||||
yield ('/'.join(filter(lambda x: len(x) > 0, path)),
|
||||
verb,
|
||||
m_router,
|
||||
*HalfDomain.gen_routes(m_router, verb, path, params[verb], PATH_PARAMS)
|
||||
)
|
||||
|
||||
for subroute in params.get('SUBROUTES', []):
|
||||
subroute_module = importlib.import_module(f'.{subroute}', m_router.__name__)
|
||||
param_match = re.fullmatch('^([A-Z_]+)_([a-z]+)$', subroute)
|
||||
parameter_name = None
|
||||
if param_match is not None:
|
||||
try:
|
||||
parameter_name = param_match.groups()[0].lower()
|
||||
if parameter_name in PATH_PARAMS:
|
||||
raise Exception(f'Duplicate parameter name in same path! {subroute} : {parameter_name}')
|
||||
|
||||
parameter_type = param_match.groups()[1]
|
||||
path.append('{{{}:{}}}'.format(
|
||||
parameter_name,
|
||||
parameter_type,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
PATH_PARAMS[parameter_name] = subroute_module.param_docstring
|
||||
except AttributeError as exc:
|
||||
PATH_PARAMS[parameter_name] = param_docstring_default(parameter_name, parameter_type)
|
||||
|
||||
except AssertionError as exc:
|
||||
raise UnknownPathParameterType(subroute) from exc
|
||||
else:
|
||||
path.append(subroute)
|
||||
|
||||
try:
|
||||
yield from HalfDomain.gen_router_routes(
|
||||
subroute_module,
|
||||
path,
|
||||
PATH_PARAMS
|
||||
)
|
||||
|
||||
except ImportError as exc:
|
||||
logger.error('Failed to import subroute **{%s}**', subroute)
|
||||
raise exc
|
||||
|
||||
path.pop()
|
||||
if parameter_name:
|
||||
PATH_PARAMS.pop(parameter_name)
|
||||
|
||||
|
||||
path.pop()
|
||||
|
||||
|
||||
@staticmethod
|
||||
def read_router(m_router: ModuleType) -> Dict:
|
||||
"""
|
||||
Reads a module and returns a router dict
|
||||
|
||||
If the module has a "ROUTES" constant, it just returns this constant,
|
||||
Else, if the module has an "ACLS" constant, it builds the accurate dict
|
||||
|
||||
TODO: May be another thing, may be not a part of halfAPI
|
||||
|
||||
"""
|
||||
m_path = None
|
||||
|
||||
try:
|
||||
if not hasattr(m_router, 'ROUTES'):
|
||||
routes = {'':{}}
|
||||
acls = getattr(m_router, 'ACLS') if hasattr(m_router, 'ACLS') else None
|
||||
|
||||
if acls is not None:
|
||||
for method in acls.keys():
|
||||
if method not in VERBS:
|
||||
raise Exception(
|
||||
'This method is not handled: {}'.format(method))
|
||||
|
||||
routes[''][method] = []
|
||||
routes[''][method] = acls[method].copy()
|
||||
|
||||
routes['']['SUBROUTES'] = []
|
||||
if hasattr(m_router, '__path__'):
|
||||
""" Module is a package
|
||||
"""
|
||||
m_path = getattr(m_router, '__path__')
|
||||
if isinstance(m_path, list) and len(m_path) == 1:
|
||||
routes['']['SUBROUTES'] = [
|
||||
elt.name
|
||||
for elt in os.scandir(m_path[0])
|
||||
if elt.is_dir()
|
||||
]
|
||||
else:
|
||||
routes = getattr(m_router, 'ROUTES')
|
||||
|
||||
try:
|
||||
ROUTER_SCHEMA.validate(routes)
|
||||
except SchemaError as exc:
|
||||
logger.error(routes)
|
||||
raise exc
|
||||
|
||||
return routes
|
||||
except ImportError as exc:
|
||||
# TODO: Proper exception handling
|
||||
raise exc
|
||||
except FileNotFoundError as exc:
|
||||
# TODO: Proper exception handling
|
||||
logger.error(m_path)
|
||||
raise exc
|
||||
|
||||
def gen_domain_routes(self):
|
||||
"""
|
||||
Yields the Route objects for a domain
|
||||
|
||||
Parameters:
|
||||
m_domains: ModuleType
|
||||
|
||||
Returns:
|
||||
Generator(HalfRoute)
|
||||
"""
|
||||
yield HalfRoute('/',
|
||||
self.schema_openapi(),
|
||||
[{'acl': lib_acl.public}],
|
||||
'GET'
|
||||
)
|
||||
|
||||
for path, method, m_router, fct, params in HalfDomain.gen_router_routes(self.m_router, []):
|
||||
yield HalfRoute(f'/{path}', fct, params, method)
|
||||
|
||||
def schema_dict(self) -> Dict:
|
||||
""" gen_router_routes return values as a dict
|
||||
Parameters:
|
||||
|
||||
m_router (ModuleType): The domain routers' module
|
||||
|
||||
Returns:
|
||||
|
||||
Dict: Schema of dict is halfapi.lib.constants.DOMAIN_SCHEMA
|
||||
|
||||
@TODO: Should be a "router_schema_dict" function
|
||||
"""
|
||||
d_res = {}
|
||||
|
||||
for path, verb, m_router, fct, parameters in HalfDomain.gen_router_routes(self.m_router, []):
|
||||
if path not in d_res:
|
||||
d_res[path] = {}
|
||||
|
||||
if verb not in d_res[path]:
|
||||
d_res[path][verb] = {}
|
||||
|
||||
d_res[path][verb]['callable'] = f'{m_router.__name__}:{fct.__name__}'
|
||||
try:
|
||||
d_res[path][verb]['docs'] = yaml.safe_load(fct.__doc__)
|
||||
except AttributeError:
|
||||
logger.error(
|
||||
'Cannot read docstring from fct (fct=%s path=%s verb=%s', fct.__name__, path, verb)
|
||||
|
||||
d_res[path][verb]['acls'] = list(map(lambda elt: { **elt, 'acl': elt['acl'].__name__ },
|
||||
parameters))
|
||||
|
||||
return d_res
|
||||
|
||||
|
||||
def schema(self) -> Dict:
|
||||
schema = { **API_SCHEMA_DICT }
|
||||
schema['domain'] = {
|
||||
'name': self.name,
|
||||
'id': self.id,
|
||||
'version': getattr(self.m_domain, '__version__', ''),
|
||||
'patch_release': getattr(self.m_domain, '__patch_release__', ''),
|
||||
'routers': self.m_router.__name__,
|
||||
'acls': tuple(getattr(self.m_acl, 'ACLS', ()))
|
||||
}
|
||||
schema['paths'] = self.schema_dict()
|
||||
return schema
|
||||
|
||||
def schema_openapi(self) -> Route:
|
||||
schema = SchemaGenerator(
|
||||
{
|
||||
'openapi': '3.0.0',
|
||||
'info': {
|
||||
'title': self.name,
|
||||
'version': self.version,
|
||||
'x-acls': tuple(getattr(self.m_acl, 'ACLS', ())),
|
||||
**({
|
||||
f'x-{key}': value
|
||||
for key, value in self.d_domain.items()
|
||||
}),
|
||||
},
|
||||
'components': self.schema_components
|
||||
}
|
||||
|
||||
)
|
||||
|
||||
async def inner(request, *args, **kwargs):
|
||||
"""
|
||||
description: |
|
||||
Returns the current API routes description (OpenAPI v3)
|
||||
as a JSON object
|
||||
responses:
|
||||
200:
|
||||
description: API Schema in OpenAPI v3 format
|
||||
"""
|
||||
return ORJSONResponse(
|
||||
schema.get_schema(routes=request.app.routes))
|
||||
|
||||
return inner
|
||||
|
@ -13,21 +13,12 @@ from starlette.routing import Route
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from .logging import logger
|
||||
from .lib.domain import MissingAclError, PathError, UnknownPathParameterType, \
|
||||
UndefinedRoute, UndefinedFunction
|
||||
|
||||
class HalfRoute(Route):
|
||||
""" HalfRoute
|
||||
"""
|
||||
def __init__(self, path: List[str], fct: Callable, params: List[Dict], method: str):
|
||||
logger.info('HalfRoute creation: %s %s %s %s', path, fct, params, method)
|
||||
if len(params) == 0:
|
||||
raise MissingAclError('[{}] {}'.format(method, '/'.join(path)))
|
||||
|
||||
if len(path) == 0:
|
||||
logger.error('Empty path for [{%s}]', method)
|
||||
raise PathError()
|
||||
|
||||
def __init__(self, path, fct, params, method):
|
||||
logger.info('HalfRoute creation: %s', params)
|
||||
super().__init__(
|
||||
path,
|
||||
HalfRoute.acl_decorator(
|
||||
@ -84,11 +75,7 @@ class HalfRoute(Route):
|
||||
logger.debug(
|
||||
'Args for current route (%s)', param.get('args'))
|
||||
|
||||
if 'out' in param:
|
||||
req.scope['out'] = param['out']
|
||||
|
||||
if 'out' in param:
|
||||
req.scope['out'] = param['out'].copy()
|
||||
|
||||
if 'check' in req.query_params:
|
||||
return PlainTextResponse(param['acl'].__name__)
|
||||
|
@ -9,7 +9,6 @@ It defines the following globals :
|
||||
- application (the asgi application itself - a starlette object)
|
||||
|
||||
"""
|
||||
import sys
|
||||
import logging
|
||||
import time
|
||||
import importlib
|
||||
@ -20,7 +19,7 @@ from starlette.applications import Starlette
|
||||
from starlette.authentication import UnauthenticatedUser
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.middleware import Middleware
|
||||
from starlette.routing import Router, Route, Mount
|
||||
from starlette.routing import Route, Mount
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response, PlainTextResponse
|
||||
from starlette.middleware.authentication import AuthenticationMiddleware
|
||||
@ -33,178 +32,124 @@ from timing_asgi.integrations import StarletteScopeToName
|
||||
from .lib.constants import API_SCHEMA_DICT
|
||||
from .lib.domain_middleware import DomainMiddleware
|
||||
from .lib.timing import HTimingClient
|
||||
from .lib.jwt_middleware import JWTAuthenticationBackend, on_auth_error
|
||||
from .lib.jwt_middleware import JWTAuthenticationBackend
|
||||
from .lib.responses import (ORJSONResponse, UnauthorizedResponse,
|
||||
NotFoundResponse, InternalServerErrorResponse, NotImplementedResponse,
|
||||
ServiceUnavailableResponse, gen_exception_route)
|
||||
from .lib.domain import NoDomainsException
|
||||
from .lib.routes import gen_schema_routes, JSONRoute
|
||||
from .lib.schemas import schema_json
|
||||
ServiceUnavailableResponse)
|
||||
from .lib.domain import domain_schema_dict, NoDomainsException, domain_schema
|
||||
from .lib.routes import gen_domain_routes, gen_schema_routes, JSONRoute
|
||||
from .lib.schemas import schema_json, get_acls
|
||||
from .logging import logger, config_logging
|
||||
from .half_domain import HalfDomain
|
||||
from halfapi import __version__
|
||||
|
||||
class HalfAPI(Starlette):
|
||||
def __init__(self,
|
||||
config,
|
||||
d_routes=None):
|
||||
# Set log level (defaults to debug)
|
||||
config_logging(
|
||||
getattr(logging, config.get('loglevel', 'DEBUG').upper(), 'DEBUG')
|
||||
)
|
||||
self.config = config
|
||||
SECRET = self.config.get('secret')
|
||||
PRODUCTION = self.config.get('production', True)
|
||||
DRYRUN = self.config.get('dryrun', False)
|
||||
TIMINGMIDDLEWARE = self.config.get('timingmiddleware', False)
|
||||
|
||||
if DRYRUN:
|
||||
logger.info('HalfAPI starting in dry-run mode')
|
||||
else:
|
||||
logger.info('HalfAPI starting')
|
||||
|
||||
class HalfAPI:
|
||||
def __init__(self, config,
|
||||
routes_dict=None):
|
||||
config_logging(logging.DEBUG)
|
||||
|
||||
SECRET = config.get('secret')
|
||||
PRODUCTION = config.get('production', True)
|
||||
CONFIG = config.get('config', {})
|
||||
|
||||
domain = config.get('domain')['name']
|
||||
router = config.get('domain').get('router', None)
|
||||
|
||||
if not (domain and router):
|
||||
raise NoDomainsException()
|
||||
|
||||
self.PRODUCTION = PRODUCTION
|
||||
self.CONFIG = CONFIG
|
||||
self.SECRET = SECRET
|
||||
|
||||
# Domains
|
||||
self.__application = None
|
||||
|
||||
m_domain = m_domain_router = m_domain_acl = None
|
||||
if domain:
|
||||
m_domain = importlib.import_module(f'{domain}')
|
||||
if not router:
|
||||
router = getattr('__router__', domain, '.routers')
|
||||
m_domain_router = importlib.import_module(router)
|
||||
m_domain_acl = importlib.import_module(f'{domain}.acl')
|
||||
|
||||
if not(m_domain and m_domain_router and m_domain_acl):
|
||||
raise Exception('Cannot import domain')
|
||||
|
||||
self.schema = domain_schema(m_domain)
|
||||
|
||||
routes = [ Route('/', JSONRoute(self.schema)) ]
|
||||
|
||||
""" HalfAPI routes (if not PRODUCTION, includes debug routes)
|
||||
"""
|
||||
routes = []
|
||||
routes.append(
|
||||
Mount('/halfapi', routes=list(self.halfapi_routes()))
|
||||
Mount('/halfapi', routes=list(self.routes()))
|
||||
)
|
||||
|
||||
logger.debug('Config: %s', self.config)
|
||||
|
||||
domains = {
|
||||
key: elt
|
||||
for key, elt in self.config.get('domain', {}).items()
|
||||
if elt.get('enabled', False)
|
||||
}
|
||||
|
||||
logger.debug('Active domains: %s', domains)
|
||||
|
||||
if d_routes:
|
||||
# Mount the routes from the d_routes argument - domain-less mode
|
||||
if routes_dict:
|
||||
# Mount the routes from the routes_dict argument - domain-less mode
|
||||
logger.info('Domain-less mode : the given schema defines the activated routes')
|
||||
for route in gen_schema_routes(d_routes):
|
||||
for route in gen_schema_routes(routes_dict):
|
||||
routes.append(route)
|
||||
else:
|
||||
pass
|
||||
for route in gen_domain_routes(m_domain_router):
|
||||
routes.append(route)
|
||||
|
||||
startup_fcts = []
|
||||
|
||||
if DRYRUN:
|
||||
startup_fcts.append(
|
||||
HalfAPI.wait_quit()
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
self.__application = Starlette(
|
||||
debug=not PRODUCTION,
|
||||
routes=routes,
|
||||
exception_handlers={
|
||||
401: gen_exception_route(UnauthorizedResponse),
|
||||
404: gen_exception_route(NotFoundResponse),
|
||||
500: gen_exception_route(HalfAPI.exception),
|
||||
501: gen_exception_route(NotImplementedResponse),
|
||||
503: gen_exception_route(ServiceUnavailableResponse)
|
||||
401: UnauthorizedResponse,
|
||||
404: NotFoundResponse,
|
||||
500: InternalServerErrorResponse,
|
||||
501: NotImplementedResponse,
|
||||
503: ServiceUnavailableResponse
|
||||
}
|
||||
)
|
||||
|
||||
schemas = []
|
||||
|
||||
self.__domains = {}
|
||||
|
||||
for key, domain in domains.items():
|
||||
if not isinstance(domain, dict):
|
||||
continue
|
||||
|
||||
dom_name = domain.get('name', key)
|
||||
if not domain.get('enabled', False):
|
||||
continue
|
||||
|
||||
if not domain.get('prefix', False):
|
||||
if len(domains.keys()) > 1:
|
||||
raise Exception('Cannot use multiple domains and set prefix to false')
|
||||
path = '/'
|
||||
else:
|
||||
path = f'/{dom_name}'
|
||||
|
||||
logger.debug('Mounting domain %s on %s', domain.get('name'), path)
|
||||
|
||||
domain_key = domain.get('name', key)
|
||||
|
||||
add_domain_args = {
|
||||
**domain,
|
||||
'path': path
|
||||
}
|
||||
|
||||
self.add_domain(**add_domain_args)
|
||||
|
||||
schemas.append(self.__domains[domain_key].schema())
|
||||
|
||||
|
||||
self.add_route('/', JSONRoute(schemas))
|
||||
self.__application.add_middleware(
|
||||
DomainMiddleware,
|
||||
domain=domain,
|
||||
config=CONFIG
|
||||
)
|
||||
|
||||
if SECRET:
|
||||
self.add_middleware(
|
||||
self.SECRET = SECRET
|
||||
self.__application.add_middleware(
|
||||
AuthenticationMiddleware,
|
||||
backend=JWTAuthenticationBackend(),
|
||||
on_error=on_auth_error
|
||||
backend=JWTAuthenticationBackend(secret_key=SECRET)
|
||||
)
|
||||
|
||||
if not PRODUCTION and TIMINGMIDDLEWARE:
|
||||
self.add_middleware(
|
||||
if not PRODUCTION:
|
||||
self.__application.add_middleware(
|
||||
TimingMiddleware,
|
||||
client=HTimingClient(),
|
||||
metric_namer=StarletteScopeToName(prefix="halfapi",
|
||||
starlette_app=self)
|
||||
starlette_app=self.__application)
|
||||
)
|
||||
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return __version__
|
||||
|
||||
async def version_async(self, request, *args, **kwargs):
|
||||
"""
|
||||
description: Version route
|
||||
responses:
|
||||
200:
|
||||
description: Currently running HalfAPI's version
|
||||
"""
|
||||
return Response(self.version)
|
||||
|
||||
@staticmethod
|
||||
async def exception(request: Request, exc: HTTPException):
|
||||
logger.critical(exc, exc_info=True)
|
||||
return InternalServerErrorResponse()
|
||||
|
||||
@property
|
||||
def application(self):
|
||||
return self
|
||||
return self.__application
|
||||
|
||||
def halfapi_routes(self):
|
||||
def routes(self):
|
||||
""" Halfapi default routes
|
||||
"""
|
||||
|
||||
async def get_user(request, *args, **kwargs):
|
||||
"""
|
||||
description: WhoAmI route
|
||||
responses:
|
||||
200:
|
||||
description: The currently logged-in user
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
"""
|
||||
return ORJSONResponse({'user':request.user})
|
||||
|
||||
yield Route('/whoami', get_user)
|
||||
yield Route('/schema', schema_json)
|
||||
yield Mount('/acls', self.acls_router())
|
||||
yield Route('/acls', get_acls)
|
||||
yield Route('/version', self.version_async)
|
||||
""" Halfapi debug routes definition
|
||||
"""
|
||||
@ -236,81 +181,3 @@ class HalfAPI(Starlette):
|
||||
@staticmethod
|
||||
def api_schema(domain):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def wait_quit():
|
||||
""" sleeps 1 second and quits. used in dry-run mode
|
||||
"""
|
||||
import time
|
||||
import sys
|
||||
time.sleep(1)
|
||||
sys.exit(0)
|
||||
|
||||
def acls_router(self):
|
||||
mounts = {}
|
||||
|
||||
for domain, domain_conf in self.config.get('domain', {}).items():
|
||||
if isinstance(domain_conf, dict) and domain_conf.get('enabled', False):
|
||||
mounts['domain'] = HalfDomain.acls_router(
|
||||
domain,
|
||||
module_path=domain_conf.get('module'),
|
||||
acl=domain_conf.get('acl')
|
||||
)
|
||||
|
||||
if len(mounts) > 1:
|
||||
return Router([
|
||||
Mount(f'/{domain}', acls_router)
|
||||
for domain, acls_router in mounts.items()
|
||||
])
|
||||
elif len(mounts) == 1:
|
||||
return Mount('/', mounts.popitem()[1])
|
||||
else:
|
||||
return Router()
|
||||
|
||||
@property
|
||||
def domains(self):
|
||||
return self.__domains
|
||||
|
||||
def add_domain(self, **kwargs):
|
||||
|
||||
if not kwargs.get('enabled'):
|
||||
raise Exception(f'Domain not enabled ({kwargs})')
|
||||
|
||||
name = kwargs['name']
|
||||
|
||||
self.config['domain'][name] = kwargs.get('config', {})
|
||||
|
||||
if not kwargs.get('module'):
|
||||
module = name
|
||||
else:
|
||||
module = kwargs.get('module')
|
||||
|
||||
try:
|
||||
self.__domains[name] = HalfDomain(
|
||||
name,
|
||||
module=importlib.import_module(module),
|
||||
router=kwargs.get('router'),
|
||||
acl=kwargs.get('acl'),
|
||||
app=self
|
||||
)
|
||||
|
||||
except ImportError as exc:
|
||||
print(
|
||||
'Cannot instantiate HalfDomain {} with module {}'.format(
|
||||
name,
|
||||
module
|
||||
))
|
||||
raise exc
|
||||
|
||||
self.mount(kwargs.get('path', name), self.__domains[name])
|
||||
|
||||
return self.__domains[name]
|
||||
|
||||
|
||||
def __main__():
|
||||
return HalfAPI(CONFIG).application
|
||||
|
||||
if __name__ == '__main__':
|
||||
__main__()
|
||||
|
||||
|
||||
|
@ -2,14 +2,10 @@
|
||||
"""
|
||||
Base ACL module that contains generic functions for domains ACL
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from functools import wraps
|
||||
from json import JSONDecodeError
|
||||
import yaml
|
||||
from starlette.authentication import UnauthenticatedUser
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.routing import Route
|
||||
from starlette.responses import Response
|
||||
|
||||
from ..logging import logger
|
||||
|
||||
@ -32,9 +28,7 @@ def connected(fct=public):
|
||||
or not hasattr(req.user, 'is_authenticated')):
|
||||
return False
|
||||
|
||||
if hasattr(req, 'path_params'):
|
||||
return fct(req, **{**kwargs, **req.path_params})
|
||||
return fct(req, **{**kwargs})
|
||||
|
||||
return caller
|
||||
|
||||
@ -60,23 +54,11 @@ def args_check(fct):
|
||||
data_ = dict(req.query_params)
|
||||
|
||||
elif req.method in ['POST', 'PATCH', 'PUT', 'DELETE']:
|
||||
if req.scope.get('headers'):
|
||||
if b'content-type' not in dict(req.scope.get('headers')):
|
||||
data_ = {}
|
||||
else:
|
||||
content_type = dict(req.scope.get('headers')).get(b'content-type').decode().split(';')[0]
|
||||
|
||||
if content_type == 'application/json':
|
||||
try:
|
||||
data_ = await req.json()
|
||||
except JSONDecodeError as exc:
|
||||
logger.debug('Posted data was not JSON')
|
||||
pass
|
||||
elif content_type in [
|
||||
'multipart/form-data', 'application/x-www-form-urlencoded']:
|
||||
data_ = dict(await req.form())
|
||||
else:
|
||||
data_ = await req.body()
|
||||
|
||||
def plural(array: list) -> str:
|
||||
return 's' if len(array) > 1 else ''
|
||||
@ -85,7 +67,7 @@ def args_check(fct):
|
||||
|
||||
|
||||
args_d = req.scope.get('args')
|
||||
if args_d is not None and isinstance(data_, dict):
|
||||
if args_d is not None:
|
||||
required = args_d.get('required', set())
|
||||
|
||||
missing = []
|
||||
@ -106,15 +88,13 @@ def args_check(fct):
|
||||
if key in data_:
|
||||
data[key] = data_[key]
|
||||
else:
|
||||
""" Unsafe mode, without specified arguments, or plain text mode
|
||||
""" Unsafe mode, without specified arguments
|
||||
"""
|
||||
data = data_
|
||||
|
||||
kwargs['data'] = data
|
||||
|
||||
out_s = req.scope.get('out')
|
||||
if out_s:
|
||||
kwargs['out'] = list(out_s)
|
||||
logger.debug('args_check %s:%s %s %s', fct.__module__, fct.__name__, args, kwargs)
|
||||
|
||||
return await fct(req, *args, **kwargs)
|
||||
|
||||
@ -122,57 +102,7 @@ def args_check(fct):
|
||||
|
||||
# ACLS list for doc and priorities
|
||||
# Write your own constant in your domain or import this one
|
||||
# Format : (acl_name: str, acl_documentation: str, priority: int, [public=False])
|
||||
#
|
||||
# The 'priority' integer is greater than zero and the lower values means more
|
||||
# priority. For a route, the order of declaration of the ACLs should respect
|
||||
# their priority.
|
||||
#
|
||||
# When the 'public' boolean value is True, a route protected by this ACL is
|
||||
# defined on the "/halfapi/acls/acl_name", that returns an empty response and
|
||||
# the status code 200 or 401.
|
||||
|
||||
ACLS = (
|
||||
('private', private.__doc__, 0, True),
|
||||
('public', public.__doc__, 999, True)
|
||||
('private', public.__doc__, 0),
|
||||
('public', public.__doc__, 999)
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class ACL():
|
||||
name: str
|
||||
documentation: str
|
||||
priority: int
|
||||
public: bool = False
|
||||
|
||||
|
||||
class AclRoute(Route):
|
||||
def __init__(self, path, acl_fct, acl: ACL):
|
||||
self.acl_fct = acl_fct
|
||||
self.name = acl.name
|
||||
self.description = acl.documentation
|
||||
|
||||
self.docstring = yaml.dump({
|
||||
'description': f'{self.name}: {self.description}',
|
||||
'responses': {
|
||||
'200': {
|
||||
'description': 'ACL OK'
|
||||
},
|
||||
'401': {
|
||||
'description': 'ACL FAIL'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async def endpoint(request, *args, **kwargs):
|
||||
if request.method == 'GET':
|
||||
logger.warning('Deprecated since 0.6.28, use HEAD method since now')
|
||||
|
||||
if self.acl_fct(request, *args, **kwargs) is True:
|
||||
return Response(status_code=200)
|
||||
|
||||
return Response(status_code=401)
|
||||
|
||||
endpoint.__doc__ = self.docstring
|
||||
|
||||
return super().__init__(path, methods=['HEAD', 'GET'], endpoint=endpoint)
|
||||
|
||||
|
@ -39,12 +39,11 @@ ROUTE_SCHEMA = Schema({
|
||||
|
||||
DOMAIN_SCHEMA = Schema({
|
||||
'name': str,
|
||||
'id': str,
|
||||
Optional('routers'): str,
|
||||
Optional('version'): str,
|
||||
Optional('patch_release'): str,
|
||||
Optional('acls'): [
|
||||
[str, str, int, Optional(bool)]
|
||||
[str, str, int]
|
||||
]
|
||||
})
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
lib/domain.py The domain-scoped utility functions
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import importlib
|
||||
@ -10,41 +11,31 @@ import inspect
|
||||
from functools import wraps
|
||||
from types import ModuleType, FunctionType
|
||||
from typing import Coroutine, Generator
|
||||
from typing import Dict, List, Tuple
|
||||
from typing import Dict, List, Tuple, Iterator
|
||||
import yaml
|
||||
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from halfapi.lib import acl
|
||||
from halfapi.lib.responses import ORJSONResponse, ODSResponse, XLSXResponse, PlainTextResponse, HTMLResponse
|
||||
# from halfapi.lib.router import read_router
|
||||
from halfapi.lib.responses import ORJSONResponse
|
||||
from halfapi.lib.router import read_router
|
||||
from halfapi.lib.constants import VERBS
|
||||
|
||||
from ..logging import logger
|
||||
|
||||
class MissingAclError(Exception):
|
||||
""" Exception to use when no acl are specified for a route
|
||||
"""
|
||||
pass
|
||||
|
||||
class PathError(Exception):
|
||||
""" Exception to use when the path for a route is malformed
|
||||
"""
|
||||
pass
|
||||
|
||||
class UnknownPathParameterType(Exception):
|
||||
""" Exception to use when the path parameter for a route is not supported
|
||||
"""
|
||||
pass
|
||||
|
||||
class UndefinedRoute(Exception):
|
||||
""" Exception to use when the route definition cannot be found
|
||||
"""
|
||||
pass
|
||||
|
||||
class UndefinedFunction(Exception):
|
||||
""" Exception to use when a function definition cannot be found
|
||||
"""
|
||||
pass
|
||||
|
||||
class NoDomainsException(Exception):
|
||||
@ -52,97 +43,43 @@ class NoDomainsException(Exception):
|
||||
"""
|
||||
pass
|
||||
|
||||
def route_decorator(fct: FunctionType) -> Coroutine:
|
||||
def route_decorator(fct: FunctionType, ret_type: str = 'json') -> Coroutine:
|
||||
""" Returns an async function that can be mounted on a router
|
||||
"""
|
||||
if ret_type == 'json':
|
||||
@wraps(fct)
|
||||
@acl.args_check
|
||||
async def wrapped(request, *args, **kwargs):
|
||||
fct_args_spec = inspect.getfullargspec(fct).args
|
||||
fct_args_defaults = inspect.getfullargspec(fct).defaults or []
|
||||
fct_args_defaults_dict = dict(list(zip(
|
||||
reversed(fct_args_spec),
|
||||
reversed(fct_args_defaults)
|
||||
)))
|
||||
|
||||
fct_args = request.path_params.copy()
|
||||
|
||||
if 'halfapi' in fct_args_spec:
|
||||
fct_args['halfapi'] = {
|
||||
'user': request.user if
|
||||
'user' in request else None,
|
||||
'config': request.scope.get('config', {}),
|
||||
'domain': request.scope.get('domain', 'unknown'),
|
||||
'cookies': request.cookies,
|
||||
'base_url': request.base_url,
|
||||
'url': request.url
|
||||
'config': request.scope['config'],
|
||||
'domain': request.scope['domain'],
|
||||
|
||||
}
|
||||
|
||||
|
||||
if 'data' in fct_args_spec:
|
||||
if 'data' in fct_args_defaults_dict:
|
||||
fct_args['data'] = fct_args_defaults_dict['data']
|
||||
else:
|
||||
fct_args['data'] = {}
|
||||
fct_args['data'] = kwargs.get('data')
|
||||
|
||||
fct_args['data'].update(kwargs.get('data', {}))
|
||||
|
||||
if 'out' in fct_args_spec:
|
||||
fct_args['out'] = kwargs.get('out')
|
||||
|
||||
""" If format argument is specified (either by get, post param or function argument)
|
||||
"""
|
||||
if 'ret_type' in fct_args_defaults_dict:
|
||||
ret_type = fct_args_defaults_dict['ret_type']
|
||||
else:
|
||||
ret_type = fct_args.get('data', {}).get('format', 'json')
|
||||
|
||||
logger.debug('Return type {} (defaults: {})'.format(ret_type,
|
||||
fct_args_defaults_dict))
|
||||
try:
|
||||
logger.debug('FCT_ARGS***** %s', fct_args)
|
||||
if ret_type == 'json':
|
||||
return ORJSONResponse(fct(**fct_args))
|
||||
|
||||
if ret_type == 'ods':
|
||||
res = fct(**fct_args)
|
||||
assert isinstance(res, list)
|
||||
for elt in res:
|
||||
assert isinstance(elt, dict)
|
||||
|
||||
return ODSResponse(res)
|
||||
|
||||
if ret_type == 'xlsx':
|
||||
res = fct(**fct_args)
|
||||
assert isinstance(res, list)
|
||||
for elt in res:
|
||||
assert isinstance(elt, dict)
|
||||
|
||||
return XLSXResponse(res)
|
||||
|
||||
if ret_type in ['html', 'xhtml']:
|
||||
res = fct(**fct_args)
|
||||
assert isinstance(res, str)
|
||||
|
||||
return HTMLResponse(res)
|
||||
|
||||
if ret_type in 'txt':
|
||||
res = fct(**fct_args)
|
||||
assert isinstance(res, str)
|
||||
|
||||
return PlainTextResponse(res)
|
||||
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
except NotImplementedError as exc:
|
||||
raise HTTPException(501) from exc
|
||||
except Exception as exc:
|
||||
# TODO: Write tests
|
||||
logger.error(exc, exc_info=True)
|
||||
if not isinstance(exc, HTTPException):
|
||||
raise HTTPException(500) from exc
|
||||
raise exc
|
||||
|
||||
|
||||
else:
|
||||
raise Exception('Return type not available')
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
@ -190,3 +127,247 @@ def get_fct_name(http_verb: str, path: str) -> str:
|
||||
|
||||
return '_'.join(fct_name)
|
||||
|
||||
def gen_routes(m_router: ModuleType,
|
||||
verb: str,
|
||||
path: List[str],
|
||||
params: List[Dict]) -> Tuple[FunctionType, Dict]:
|
||||
"""
|
||||
Returns a tuple of the function associatied to the verb and path arguments,
|
||||
and the dictionary of it's acls
|
||||
|
||||
Parameters:
|
||||
- m_router (ModuleType): The module containing the function definition
|
||||
|
||||
- verb (str): The HTTP verb for the route (GET, POST, ...)
|
||||
|
||||
- path (List): The route path, as a list (each item being a level of
|
||||
deepness), from the lowest level (domain) to the highest
|
||||
|
||||
- params (Dict): The acl list of the following format :
|
||||
[{'acl': Function, 'args': {'required': [], 'optional': []}}]
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
(Function, Dict): The destination function and the acl dictionary
|
||||
|
||||
"""
|
||||
if len(params) == 0:
|
||||
raise MissingAclError('[{}] {}'.format(verb, '/'.join(path)))
|
||||
|
||||
if len(path) == 0:
|
||||
logger.error('Empty path for [{%s}]', verb)
|
||||
raise PathError()
|
||||
|
||||
fct_name = get_fct_name(verb, path[-1])
|
||||
if hasattr(m_router, fct_name):
|
||||
fct = getattr(m_router, fct_name)
|
||||
else:
|
||||
raise UndefinedFunction('{}.{}'.format(m_router.__name__, fct_name or ''))
|
||||
|
||||
|
||||
if not inspect.iscoroutinefunction(fct):
|
||||
return route_decorator(fct), params
|
||||
else:
|
||||
return acl.args_check(fct), params
|
||||
|
||||
|
||||
def gen_router_routes(m_router: ModuleType, path: List[str]) -> \
|
||||
Iterator[Tuple[str, str, ModuleType, Coroutine, List]]:
|
||||
"""
|
||||
Recursive generator that parses a router (or a subrouter)
|
||||
and yields from gen_routes
|
||||
|
||||
Parameters:
|
||||
|
||||
- m_router (ModuleType): The currently treated router module
|
||||
- path (List[str]): The current path stack
|
||||
|
||||
Yields:
|
||||
|
||||
(str, str, ModuleType, Coroutine, List): A tuple containing the path, verb,
|
||||
router module, function reference and parameters of the route.
|
||||
Function and parameters are yielded from then gen_routes function,
|
||||
that decorates the endpoint function.
|
||||
"""
|
||||
|
||||
for subpath, params in read_router(m_router).items():
|
||||
path.append(subpath)
|
||||
|
||||
for verb in VERBS:
|
||||
if verb not in params:
|
||||
continue
|
||||
yield ('/'.join(filter(lambda x: len(x) > 0, path)),
|
||||
verb,
|
||||
m_router,
|
||||
*gen_routes(m_router, verb, path, params[verb])
|
||||
)
|
||||
|
||||
for subroute in params.get('SUBROUTES', []):
|
||||
#logger.debug('Processing subroute **%s** - %s', subroute, m_router.__name__)
|
||||
param_match = re.fullmatch('^([A-Z_]+)_([a-z]+)$', subroute)
|
||||
if param_match is not None:
|
||||
try:
|
||||
path.append('{{{}:{}}}'.format(
|
||||
param_match.groups()[0].lower(),
|
||||
param_match.groups()[1]))
|
||||
except AssertionError as exc:
|
||||
raise UnknownPathParameterType(subroute) from exc
|
||||
else:
|
||||
path.append(subroute)
|
||||
|
||||
try:
|
||||
yield from gen_router_routes(
|
||||
importlib.import_module(f'.{subroute}', m_router.__name__),
|
||||
path)
|
||||
|
||||
except ImportError as exc:
|
||||
logger.error('Failed to import subroute **{%s}**', subroute)
|
||||
raise exc
|
||||
|
||||
path.pop()
|
||||
|
||||
path.pop()
|
||||
|
||||
|
||||
|
||||
def domain_schema_dict(m_router: ModuleType) -> Dict:
|
||||
""" gen_router_routes return values as a dict
|
||||
Parameters:
|
||||
|
||||
m_router (ModuleType): The domain routers' module
|
||||
|
||||
Returns:
|
||||
|
||||
Dict: Schema of dict is halfapi.lib.constants.DOMAIN_SCHEMA
|
||||
|
||||
@TODO: Should be a "router_schema_dict" function
|
||||
"""
|
||||
d_res = {}
|
||||
|
||||
for path, verb, m_router, fct, parameters in gen_router_routes(m_router, []):
|
||||
if path not in d_res:
|
||||
d_res[path] = {}
|
||||
|
||||
if verb not in d_res[path]:
|
||||
d_res[path][verb] = {}
|
||||
|
||||
d_res[path][verb]['callable'] = f'{m_router.__name__}:{fct.__name__}'
|
||||
d_res[path][verb]['docs'] = yaml.safe_load(fct.__doc__)
|
||||
d_res[path][verb]['acls'] = list(map(lambda elt: { **elt, 'acl': elt['acl'].__name__ },
|
||||
parameters))
|
||||
|
||||
return d_res
|
||||
|
||||
from .constants import API_SCHEMA_DICT
|
||||
def domain_schema(m_domain: ModuleType) -> Dict:
|
||||
schema = { **API_SCHEMA_DICT }
|
||||
routers_submod_str = getattr(m_domain, '__routers__', '.routers')
|
||||
m_domain_acl = importlib.import_module('.acl', m_domain.__package__)
|
||||
m_domain_routers = importlib.import_module(
|
||||
routers_submod_str, m_domain.__package__
|
||||
)
|
||||
schema['domain'] = {
|
||||
'name': getattr(m_domain, '__name__'),
|
||||
'version': getattr(m_domain, '__version__', ''),
|
||||
'patch_release': getattr(m_domain, '__patch_release__', ''),
|
||||
'routers': routers_submod_str,
|
||||
'acls': tuple(getattr(m_domain_acl, 'ACLS', ()))
|
||||
}
|
||||
schema['paths'] = domain_schema_dict(m_domain_routers)
|
||||
return schema
|
||||
|
||||
def domain_schema_list(m_router: ModuleType) -> List:
|
||||
""" Schema as list, one row by route/acl
|
||||
Parameters:
|
||||
|
||||
m_router (ModuleType): The domain routers' module
|
||||
|
||||
Returns:
|
||||
|
||||
List[Tuple]: (path, verb, callable, doc, acls)
|
||||
"""
|
||||
res = []
|
||||
|
||||
for path, verb, m_router, fct, parameters in gen_router_routes(m_router, []):
|
||||
for params in parameters:
|
||||
res.append((
|
||||
path,
|
||||
verb,
|
||||
f'{m_router.__name__}:{fct.__name__}',
|
||||
params.get('acl').__name__,
|
||||
params.get('args', {}).get('required', []),
|
||||
params.get('args', {}).get('optional', []),
|
||||
params.get('out', [])
|
||||
))
|
||||
|
||||
return res
|
||||
|
||||
|
||||
|
||||
def d_domains(config) -> Dict[str, ModuleType]:
|
||||
"""
|
||||
Parameters:
|
||||
|
||||
config (ConfigParser): The .halfapi/config based configparser object
|
||||
|
||||
Returns:
|
||||
|
||||
dict[str, ModuleType]
|
||||
"""
|
||||
|
||||
domains = {}
|
||||
|
||||
if os.environ.get('HALFAPI_DOMAIN_NAME') and os.environ.get('HALFAPI_DOMAIN_MODULE', '.routers'):
|
||||
domains[os.environ.get('HALFAPI_DOMAIN_NAME')] = os.environ.get('HALFAPI_DOMAIN_MODULE')
|
||||
elif 'domains' in config:
|
||||
domains = dict(config['domains'].items())
|
||||
|
||||
try:
|
||||
return {
|
||||
domain: importlib.import_module(''.join((domain, module)))
|
||||
for domain, module in domains.items()
|
||||
}
|
||||
except ImportError as exc:
|
||||
logger.error('Could not load a domain : %s', exc)
|
||||
raise exc
|
||||
|
||||
def router_acls(route_params: Dict, path: List, m_router: ModuleType) -> Generator:
|
||||
for verb in VERBS:
|
||||
params = route_params.get(verb)
|
||||
if params is None:
|
||||
continue
|
||||
if len(params) == 0:
|
||||
logger.error('No ACL for route [{%s}] %s', verb, "/".join(path))
|
||||
else:
|
||||
for param in params:
|
||||
fct_acl = param.get('acl')
|
||||
if not isinstance(fct_acl, FunctionType):
|
||||
continue
|
||||
|
||||
yield fct_acl.__name__, fct_acl
|
||||
|
||||
|
||||
def domain_acls(m_router, path):
|
||||
routes = read_router(m_router)
|
||||
|
||||
for subpath, route_params in routes.items():
|
||||
path.append(subpath)
|
||||
|
||||
yield from router_acls(route_params, path, m_router)
|
||||
|
||||
subroutes = route_params.get('SUBROUTES', [])
|
||||
for subroute in subroutes:
|
||||
logger.debug('Processing subroute **%s** - %s', subroute, m_router.__name__)
|
||||
path.append(subroute)
|
||||
try:
|
||||
submod = importlib.import_module(f'.{subroute}', m_router.__name__)
|
||||
except ImportError as exc:
|
||||
logger.error('Failed to import subroute **{%s}**', subroute)
|
||||
raise exc
|
||||
|
||||
yield from domain_acls(submod, path)
|
||||
|
||||
path.pop()
|
||||
|
||||
path.pop()
|
||||
|
@ -17,12 +17,11 @@ class DomainMiddleware(BaseHTTPMiddleware):
|
||||
- acl
|
||||
"""
|
||||
|
||||
def __init__(self, app, domain=None):
|
||||
""" app: HalfAPI instance
|
||||
"""
|
||||
logger.info('DomainMiddleware app:%s domain:%s', app, domain)
|
||||
def __init__(self, app, domain, config):
|
||||
logger.info('DomainMiddleware %s %s', domain, config)
|
||||
super().__init__(app)
|
||||
self.domain = domain
|
||||
self.config = config
|
||||
self.request = None
|
||||
|
||||
|
||||
@ -32,27 +31,9 @@ class DomainMiddleware(BaseHTTPMiddleware):
|
||||
Call of the route fonction (decorated or not)
|
||||
"""
|
||||
|
||||
request.scope['domain'] = self.domain['name']
|
||||
if hasattr(request.app, 'config') \
|
||||
and isinstance(request.app.config, dict):
|
||||
# Set the config scope to the domain's config
|
||||
request.scope['config'] = request.app.config.get(
|
||||
'domain', {}
|
||||
).get(
|
||||
self.domain['name'], {}
|
||||
).copy()
|
||||
|
||||
# TODO: Remove in 0.7.0
|
||||
config = request.scope['config'].copy()
|
||||
request.scope['config']['domain'] = {}
|
||||
request.scope['config']['domain'][self.domain['name']] = {}
|
||||
request.scope['config']['domain'][self.domain['name']]['config'] = config
|
||||
|
||||
|
||||
|
||||
else:
|
||||
logger.debug('%s', request.app)
|
||||
logger.debug('%s', getattr(request.app, 'config', None))
|
||||
request.scope['domain'] = self.domain
|
||||
request.scope['config'] = self.config['domain_config'][self.domain] \
|
||||
if self.domain in self.config.get('domain_config', {}) else {}
|
||||
|
||||
response = await call_next(request)
|
||||
|
||||
@ -70,11 +51,6 @@ class DomainMiddleware(BaseHTTPMiddleware):
|
||||
response.headers['x-args-optional'] = \
|
||||
','.join(request.scope['args']['optional'])
|
||||
|
||||
if len(request.scope.get('out', set())):
|
||||
response.headers['x-out'] = \
|
||||
','.join(request.scope['out'])
|
||||
|
||||
|
||||
response.headers['x-domain'] = self.domain['name']
|
||||
response.headers['x-domain'] = self.domain
|
||||
|
||||
return response
|
||||
|
@ -14,41 +14,23 @@ from os import environ
|
||||
import typing
|
||||
from uuid import UUID
|
||||
|
||||
from http.cookies import SimpleCookie
|
||||
import jwt
|
||||
from starlette.authentication import (
|
||||
AuthenticationBackend, AuthenticationError, BaseUser, AuthCredentials,
|
||||
UnauthenticatedUser)
|
||||
from starlette.requests import HTTPConnection, Request
|
||||
from starlette.requests import HTTPConnection
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from .user import CheckUser, JWTUser, Nobody
|
||||
from ..logging import logger
|
||||
from ..conf import CONFIG
|
||||
from ..lib.responses import ORJSONResponse
|
||||
|
||||
SECRET=None
|
||||
|
||||
try:
|
||||
with open(CONFIG.get('secret', ''), 'r') as secret_file:
|
||||
SECRET = secret_file.read().strip()
|
||||
except FileNotFoundError:
|
||||
from ..conf import SECRET
|
||||
except ImportError as exc:
|
||||
logger.error('Could not import SECRET variable from conf module,'\
|
||||
' using HALFAPI_SECRET environment variable')
|
||||
|
||||
def cookies_from_scope(scope):
|
||||
cookie = dict(scope.get("headers") or {}).get(b"cookie")
|
||||
if not cookie:
|
||||
return {}
|
||||
|
||||
simple_cookie = SimpleCookie()
|
||||
simple_cookie.load(cookie.decode("utf8"))
|
||||
return {key: morsel.value for key, morsel in simple_cookie.items()}
|
||||
|
||||
def on_auth_error(request: Request, exc: Exception):
|
||||
response = ORJSONResponse({"error": str(exc)}, status_code=401)
|
||||
response.delete_cookie('Authorization')
|
||||
return response
|
||||
|
||||
class JWTAuthenticationBackend(AuthenticationBackend):
|
||||
def __init__(self, secret_key: str = SECRET,
|
||||
@ -68,22 +50,17 @@ class JWTAuthenticationBackend(AuthenticationBackend):
|
||||
self, conn: HTTPConnection
|
||||
) -> typing.Optional[typing.Tuple['AuthCredentials', 'BaseUser']]:
|
||||
|
||||
# Standard way to authenticate via API
|
||||
# https://datatracker.ietf.org/doc/html/rfc7235#section-4.2
|
||||
|
||||
token = conn.headers.get('Authorization')
|
||||
|
||||
if not token:
|
||||
token = cookies_from_scope(conn.scope).get('Authorization')
|
||||
|
||||
is_check_call = 'check' in conn.query_params
|
||||
|
||||
is_fake_user_id = is_check_call and 'user_id' in conn.query_params
|
||||
PRODUCTION = conn.scope['app'].debug == False
|
||||
|
||||
if not token and not is_check_call:
|
||||
return AuthCredentials(), Nobody()
|
||||
|
||||
try:
|
||||
if token:
|
||||
if token and not is_fake_user_id:
|
||||
payload = jwt.decode(token,
|
||||
key=self.secret_key,
|
||||
algorithms=[self.algorithm],
|
||||
@ -92,9 +69,17 @@ class JWTAuthenticationBackend(AuthenticationBackend):
|
||||
})
|
||||
|
||||
if is_check_call:
|
||||
if is_fake_user_id:
|
||||
try:
|
||||
fake_user_id = UUID(conn.query_params['user_id'])
|
||||
|
||||
return AuthCredentials(), CheckUser(fake_user_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(400, 'user_id parameter not an uuid')
|
||||
|
||||
if token:
|
||||
return AuthCredentials(), CheckUser(payload['user_id'])
|
||||
|
||||
else:
|
||||
return AuthCredentials(), Nobody()
|
||||
|
||||
|
||||
@ -102,8 +87,6 @@ class JWTAuthenticationBackend(AuthenticationBackend):
|
||||
raise AuthenticationError(
|
||||
'Trying to connect using *DEBUG* token in *PRODUCTION* mode')
|
||||
|
||||
except jwt.ExpiredSignatureError as exc:
|
||||
return AuthCredentials(), Nobody()
|
||||
except jwt.InvalidTokenError as exc:
|
||||
raise AuthenticationError(str(exc)) from exc
|
||||
except Exception as exc:
|
||||
|
@ -56,8 +56,10 @@ def parse_query(q_string: str = ""):
|
||||
>>> parse_query('limit:10')
|
||||
<function parse_query.<locals>.select at 0x...>
|
||||
|
||||
# >>> parse_query('limit=10')
|
||||
# starlette.exceptions.HTTPException: 400
|
||||
>>> parse_query('limit=10')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
starlette.exceptions.HTTPException: 400
|
||||
|
||||
|
||||
"""
|
||||
|
@ -13,22 +13,16 @@ Classes :
|
||||
- PlainTextResponse
|
||||
- ServiceUnavailableResponse
|
||||
- UnauthorizedResponse
|
||||
- ODSResponse
|
||||
|
||||
"""
|
||||
from datetime import date
|
||||
import decimal
|
||||
import typing
|
||||
from io import BytesIO
|
||||
import orjson
|
||||
|
||||
# asgi framework
|
||||
from starlette.responses import PlainTextResponse, Response, JSONResponse, HTMLResponse
|
||||
from starlette.requests import Request
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.responses import PlainTextResponse, Response, JSONResponse
|
||||
|
||||
from .user import JWTUser, Nobody
|
||||
from ..logging import logger
|
||||
|
||||
|
||||
__all__ = [
|
||||
@ -101,8 +95,6 @@ class ORJSONResponse(JSONResponse):
|
||||
JWTUser, Nobody
|
||||
}
|
||||
|
||||
if callable(typ):
|
||||
return typ.__name__
|
||||
if type(typ) in str_types:
|
||||
return str(typ)
|
||||
if type(typ) in list_types:
|
||||
@ -118,50 +110,3 @@ class HJSONResponse(ORJSONResponse):
|
||||
"""
|
||||
def render(self, content: typing.Generator):
|
||||
return super().render(list(content))
|
||||
|
||||
class ODSResponse(Response):
|
||||
file_type = 'ods'
|
||||
|
||||
def __init__(self, d_rows: typing.List[typing.Dict]):
|
||||
try:
|
||||
import pyexcel as pe
|
||||
except ImportError:
|
||||
""" ODSResponse is not handled
|
||||
"""
|
||||
super().__init__(content=
|
||||
'pyexcel is not installed, ods format not available'
|
||||
)
|
||||
return
|
||||
|
||||
with BytesIO() as ods_file:
|
||||
rows = []
|
||||
if len(d_rows):
|
||||
rows_names = list(d_rows[0].keys())
|
||||
for elt in d_rows:
|
||||
rows.append(list(elt.values()))
|
||||
|
||||
rows.insert(0, rows_names)
|
||||
|
||||
self.sheet = pe.Sheet(rows)
|
||||
self.sheet.save_to_memory(
|
||||
file_type=self.file_type,
|
||||
stream=ods_file)
|
||||
|
||||
filename = f'{date.today()}.{self.file_type}'
|
||||
|
||||
super().__init__(
|
||||
content=ods_file.getvalue(),
|
||||
headers={
|
||||
'Content-Type': 'application/vnd.oasis.opendocument.spreadsheet; charset=UTF-8',
|
||||
'Content-Disposition': f'attachment; filename="{filename}"'},
|
||||
status_code = 200)
|
||||
|
||||
|
||||
class XLSXResponse(ODSResponse):
|
||||
file_type = 'xlsx'
|
||||
|
||||
def gen_exception_route(response_cls):
|
||||
async def exception_route(req: Request, exc: HTTPException):
|
||||
return response_cls()
|
||||
|
||||
return exception_route
|
||||
|
@ -9,4 +9,63 @@ from schema import SchemaError
|
||||
from .constants import VERBS, ROUTER_SCHEMA
|
||||
from ..logging import logger
|
||||
|
||||
def read_router(m_router: ModuleType) -> Dict:
|
||||
"""
|
||||
Reads a module and returns a router dict
|
||||
|
||||
If the module has a "ROUTES" constant, it just returns this constant,
|
||||
Else, if the module has an "ACLS" constant, it builds the accurate dict
|
||||
|
||||
TODO: May be another thing, may be not a part of halfAPI
|
||||
|
||||
"""
|
||||
m_path = None
|
||||
|
||||
try:
|
||||
if not hasattr(m_router, 'ROUTES'):
|
||||
routes = {'':{}}
|
||||
acls = getattr(m_router, 'ACLS') if hasattr(m_router, 'ACLS') else None
|
||||
|
||||
if acls is not None:
|
||||
for verb in VERBS:
|
||||
if not hasattr(m_router, verb.lower()):
|
||||
# verb in function names are lowercase
|
||||
continue
|
||||
|
||||
""" There is a "verb" route in the router
|
||||
"""
|
||||
|
||||
if verb.upper() not in acls:
|
||||
continue
|
||||
|
||||
routes[''][verb.upper()] = []
|
||||
routes[''][verb.upper()] = acls[verb.upper()].copy()
|
||||
|
||||
routes['']['SUBROUTES'] = []
|
||||
if hasattr(m_router, '__path__'):
|
||||
""" Module is a package
|
||||
"""
|
||||
m_path = getattr(m_router, '__path__')
|
||||
if isinstance(m_path, list) and len(m_path) == 1:
|
||||
routes['']['SUBROUTES'] = [
|
||||
elt.name
|
||||
for elt in os.scandir(m_path[0])
|
||||
if elt.is_dir()
|
||||
]
|
||||
else:
|
||||
routes = getattr(m_router, 'ROUTES')
|
||||
|
||||
try:
|
||||
ROUTER_SCHEMA.validate(routes)
|
||||
except SchemaError as exc:
|
||||
logger.error(routes)
|
||||
raise exc
|
||||
|
||||
return routes
|
||||
except ImportError as exc:
|
||||
# TODO: Proper exception handling
|
||||
raise exc
|
||||
except FileNotFoundError as exc:
|
||||
# TODO: Proper exception handling
|
||||
logger.error(m_path)
|
||||
raise exc
|
||||
|
@ -2,13 +2,11 @@
|
||||
"""
|
||||
Routes module
|
||||
|
||||
Classes :
|
||||
- JSONRoute
|
||||
|
||||
Fonctions :
|
||||
- gen_domain_routes
|
||||
- gen_schema_routes
|
||||
- gen_starlette_routes
|
||||
- api_routes
|
||||
- api_acls
|
||||
|
||||
Exception :
|
||||
- DomainNotFoundError
|
||||
@ -21,11 +19,11 @@ from types import ModuleType, FunctionType
|
||||
|
||||
import yaml
|
||||
|
||||
# from .domain import gen_router_routes, domain_acls, route_decorator, domain_schema
|
||||
from .domain import gen_router_routes, domain_acls, route_decorator
|
||||
from .responses import ORJSONResponse
|
||||
from .acl import args_check
|
||||
from ..half_route import HalfRoute
|
||||
from . import acl
|
||||
from ..conf import DOMAINSDICT
|
||||
|
||||
from ..logging import logger
|
||||
|
||||
@ -60,12 +58,6 @@ def gen_domain_routes(m_domain: ModuleType):
|
||||
Returns:
|
||||
Generator(HalfRoute)
|
||||
"""
|
||||
yield HalfRoute('/',
|
||||
JSONRoute(domain_schema(m_domain)),
|
||||
[{'acl': acl.public}],
|
||||
'GET'
|
||||
)
|
||||
|
||||
for path, method, m_router, fct, params in gen_router_routes(m_domain, []):
|
||||
yield HalfRoute(f'/{path}', fct, params, method)
|
||||
|
||||
@ -85,6 +77,20 @@ def gen_schema_routes(schema: Dict):
|
||||
yield HalfRoute(path, args_check(fct), acls, verb)
|
||||
|
||||
|
||||
def gen_starlette_routes(d_domains: Dict[str, ModuleType]) -> Generator:
|
||||
"""
|
||||
Yields the Route objects for HalfAPI app
|
||||
|
||||
Parameters:
|
||||
d_domains (dict[str, ModuleType])
|
||||
|
||||
Returns:
|
||||
Generator(Route)
|
||||
"""
|
||||
for domain_name, m_domain in d_domains.items():
|
||||
yield from gen_domain_routes(m_domain)
|
||||
|
||||
|
||||
def api_routes(m_dom: ModuleType) -> Tuple[Dict, Dict]:
|
||||
"""
|
||||
Yields the description objects for HalfAPI app routes
|
||||
@ -134,3 +140,26 @@ def api_routes(m_dom: ModuleType) -> Tuple[Dict, Dict]:
|
||||
raise exc
|
||||
|
||||
return d_res, d_acls
|
||||
|
||||
|
||||
def api_acls(request):
|
||||
""" Returns the list of possible ACLs
|
||||
"""
|
||||
res = {}
|
||||
domains = DOMAINSDICT()
|
||||
doc = 'doc' in request.query_params
|
||||
for domain, m_domain in domains.items():
|
||||
res[domain] = {}
|
||||
for acl_name, fct in domain_acls(m_domain, [domain]):
|
||||
if not isinstance(fct, FunctionType):
|
||||
continue
|
||||
|
||||
fct_result = fct.__doc__.strip() if doc and fct.__doc__ else fct(request)
|
||||
if acl_name in res[domain]:
|
||||
continue
|
||||
|
||||
if isinstance(fct_result, FunctionType):
|
||||
fct_result = fct()(request)
|
||||
res[domain][acl_name] = fct_result
|
||||
|
||||
return res
|
||||
|
@ -13,13 +13,13 @@ import os
|
||||
import importlib
|
||||
from typing import Dict, Coroutine, List
|
||||
from types import ModuleType
|
||||
import yaml
|
||||
|
||||
from starlette.schemas import SchemaGenerator
|
||||
|
||||
from .. import __version__
|
||||
from .domain import gen_router_routes, domain_schema_list
|
||||
from ..logging import logger
|
||||
from .routes import api_routes
|
||||
from .routes import gen_starlette_routes, api_routes, api_acls
|
||||
from .responses import ORJSONResponse
|
||||
|
||||
SCHEMAS = SchemaGenerator(
|
||||
@ -28,17 +28,74 @@ SCHEMAS = SchemaGenerator(
|
||||
|
||||
async def schema_json(request, *args, **kwargs):
|
||||
"""
|
||||
description: |
|
||||
Returns the current API routes description (OpenAPI v3)
|
||||
description: Returns the current API routes description (OpenAPI v3)
|
||||
as a JSON object
|
||||
responses:
|
||||
200:
|
||||
description: API Schema in OpenAPI v3 format
|
||||
"""
|
||||
return ORJSONResponse(
|
||||
SCHEMAS.get_schema(routes=request.app.routes))
|
||||
|
||||
|
||||
def schema_dict_dom(d_domains: Dict[str, ModuleType]) -> Dict:
|
||||
"""
|
||||
Returns the API schema of the *m_domain* domain as a python dictionnary
|
||||
|
||||
Parameters:
|
||||
|
||||
d_domains (Dict[str, moduleType]): The module to scan for routes
|
||||
|
||||
Returns:
|
||||
|
||||
Dict: A dictionnary containing the description of the API using the
|
||||
| OpenAPI standard
|
||||
"""
|
||||
return SCHEMAS.get_schema(
|
||||
routes=list(gen_starlette_routes(d_domains)))
|
||||
|
||||
|
||||
async def get_acls(request, *args, **kwargs):
|
||||
"""
|
||||
description: A dictionnary of the domains and their acls, with the
|
||||
result of the acls functions
|
||||
"""
|
||||
return ORJSONResponse(api_acls(request))
|
||||
|
||||
|
||||
def schema_to_csv(module_name, header=True) -> str:
|
||||
"""
|
||||
Returns a string composed where each line is a set of path, verb, function,
|
||||
acl, required arguments, optional arguments and output variables. Those
|
||||
lines should be unique in the result string;
|
||||
"""
|
||||
# retrieve module
|
||||
m_router = importlib.import_module(module_name)
|
||||
lines = []
|
||||
if header:
|
||||
lines.append([
|
||||
'path',
|
||||
'method',
|
||||
'module:function',
|
||||
'acl',
|
||||
'args_required', 'args_optional',
|
||||
'out'
|
||||
])
|
||||
|
||||
for line in domain_schema_list(m_router):
|
||||
lines.append([
|
||||
line[0],
|
||||
line[1],
|
||||
line[2],
|
||||
line[3],
|
||||
','.join(line[4]),
|
||||
','.join(line[5]),
|
||||
','.join(line[6])
|
||||
])
|
||||
|
||||
return '\n'.join(
|
||||
[ ';'.join(fields) for fields in lines ]
|
||||
)
|
||||
|
||||
|
||||
|
||||
def schema_csv_dict(csv: List[str], prefix='/') -> Dict:
|
||||
package = None
|
||||
schema_d = {}
|
||||
@ -115,23 +172,3 @@ def schema_csv_dict(csv: List[str], prefix='/') -> Dict:
|
||||
})
|
||||
|
||||
return schema_d
|
||||
|
||||
def param_docstring_default(name, type):
|
||||
""" Returns a default docstring in OpenAPI format for a path parameter
|
||||
"""
|
||||
type_map = {
|
||||
'str': 'string',
|
||||
'uuid': 'string',
|
||||
'path': 'string',
|
||||
'int': 'number',
|
||||
'float': 'number'
|
||||
}
|
||||
return yaml.dump({
|
||||
'name': name,
|
||||
'in': 'path',
|
||||
'description': f'default description for path parameter {name}',
|
||||
'required': True,
|
||||
'schema': {
|
||||
'type': type_map[type]
|
||||
}
|
||||
})
|
||||
|
@ -1,10 +1,8 @@
|
||||
import logging
|
||||
|
||||
default_level = logging.DEBUG
|
||||
default_format = '%(asctime)s [%(process)d] [%(levelname)s] %(message)s'
|
||||
default_datefmt = '[%Y-%m-%d %H:%M:%S %z]'
|
||||
|
||||
def config_logging(level=default_level, format=default_format, datefmt=default_datefmt):
|
||||
def config_logging(level=logging.INFO):
|
||||
|
||||
# When run by 'uvicorn ...', a root handler is already
|
||||
# configured and the basicConfig below does nothing.
|
||||
# To get the desired formatting:
|
||||
@ -14,8 +12,8 @@ def config_logging(level=default_level, format=default_format, datefmt=default_d
|
||||
# https://github.com/encode/uvicorn/issues/511
|
||||
logging.basicConfig(
|
||||
# match gunicorn format
|
||||
format=format,
|
||||
datefmt=datefmt,
|
||||
format='%(asctime)s [%(process)d] [%(levelname)s] %(message)s',
|
||||
datefmt='[%Y-%m-%d %H:%M:%S %z]',
|
||||
level=level)
|
||||
|
||||
# When run by 'gunicorn -k uvicorn.workers.UvicornWorker ...',
|
||||
@ -29,4 +27,5 @@ def config_logging(level=default_level, format=default_format, datefmt=default_d
|
||||
logging.getLogger('uvicorn.access').propagate = True
|
||||
logging.getLogger('uvicorn.error').propagate = True
|
||||
|
||||
config_logging()
|
||||
logger = logging.getLogger()
|
||||
|
75
halfapi/models/api.sql
Normal file
75
halfapi/models/api.sql
Normal file
@ -0,0 +1,75 @@
|
||||
create schema api;
|
||||
|
||||
create type verb as enum ('POST', 'GET', 'PUT', 'PATCH', 'DELETE');
|
||||
|
||||
create table api.domain (
|
||||
name text,
|
||||
primary key (name)
|
||||
);
|
||||
|
||||
create table api.router (
|
||||
name text,
|
||||
domain text,
|
||||
primary key (name, domain)
|
||||
);
|
||||
|
||||
alter table api.router add constraint router_domain_fkey foreign key (domain) references api.domain(name) on update cascade on delete cascade;
|
||||
|
||||
create table api.route (
|
||||
http_verb verb,
|
||||
path text, -- relative to /<domain>/<router>
|
||||
fct_name text,
|
||||
router text,
|
||||
domain text,
|
||||
primary key (http_verb, path, router, domain)
|
||||
);
|
||||
|
||||
alter table api.route add constraint route_router_fkey foreign key (router, domain) references api.router(name, domain) on update cascade on delete cascade;
|
||||
|
||||
create table api.acl_function (
|
||||
name text,
|
||||
description text,
|
||||
domain text,
|
||||
primary key (name, domain)
|
||||
);
|
||||
|
||||
alter table api.acl_function add constraint acl_function_domain_fkey foreign key (domain) references api.domain(name) on update cascade on delete cascade;
|
||||
|
||||
create table api.acl (
|
||||
http_verb verb,
|
||||
path text not null,
|
||||
router text,
|
||||
domain text,
|
||||
acl_fct_name text,
|
||||
keys text[],
|
||||
primary key (http_verb, path, router, domain, acl_fct_name)
|
||||
);
|
||||
|
||||
alter table api.acl add constraint acl_route_fkey foreign key (http_verb, path,
|
||||
router, domain) references api.route(http_verb, path, router, domain) on update cascade on delete cascade;
|
||||
alter table api.acl add constraint acl_function_fkey foreign key (acl_fct_name, domain) references api.acl_function(name, domain) on update cascade on delete cascade;
|
||||
|
||||
create schema "api.view";
|
||||
|
||||
create view "api.view".route as
|
||||
select
|
||||
route.*,
|
||||
'/'::text || route.domain || '/'::text || route.router || route.path AS abs_path
|
||||
from
|
||||
api.route
|
||||
join api.domain on
|
||||
route.domain = domain.name
|
||||
;
|
||||
|
||||
create view "api.view".acl as
|
||||
select
|
||||
acl.*,
|
||||
'/'::text || route.domain || '/'::text || route.router || route.path AS abs_path
|
||||
from
|
||||
api.acl
|
||||
join api.acl_function on
|
||||
acl.acl_fct_name = acl_function.name
|
||||
join api.route on
|
||||
acl.domain = route.domain
|
||||
and acl.router = route.router
|
||||
and acl.path = route.path;
|
@ -1,152 +0,0 @@
|
||||
import importlib
|
||||
import functools
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from json.decoder import JSONDecodeError
|
||||
import toml
|
||||
from unittest import TestCase
|
||||
from starlette.testclient import TestClient
|
||||
from click.testing import CliRunner
|
||||
from ..cli.cli import cli
|
||||
from ..halfapi import HalfAPI
|
||||
from ..half_domain import HalfDomain
|
||||
from ..conf import DEFAULT_CONF
|
||||
from pprint import pprint
|
||||
import tempfile
|
||||
|
||||
class TestDomain(TestCase):
|
||||
@property
|
||||
def domain_name(self):
|
||||
return getattr(self, 'DOMAIN')
|
||||
|
||||
@property
|
||||
def module_name(self):
|
||||
return getattr(self, 'MODULE', self.domain_name)
|
||||
|
||||
@property
|
||||
def acl_path(self):
|
||||
return getattr(self, 'ACL', '.acl')
|
||||
|
||||
@property
|
||||
def router_path(self):
|
||||
return getattr(self, 'ROUTERS', '.routers')
|
||||
|
||||
@property
|
||||
def router_module(self):
|
||||
return '.'.join((self.module_name, self.ROUTERS))
|
||||
|
||||
|
||||
def setUp(self):
|
||||
# CLI
|
||||
class_ = CliRunner
|
||||
def invoke_wrapper(f):
|
||||
"""Augment CliRunner.invoke to emit its output to stdout.
|
||||
|
||||
This enables pytest to show the output in its logs on test
|
||||
failures.
|
||||
|
||||
"""
|
||||
@functools.wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
echo = kwargs.pop('echo', False)
|
||||
result = f(*args, **kwargs)
|
||||
|
||||
if echo is True:
|
||||
sys.stdout.write(result.output)
|
||||
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
class_.invoke = invoke_wrapper(class_.invoke)
|
||||
self.runner = class_()
|
||||
|
||||
# HTTP
|
||||
# Fake default values of default configuration
|
||||
self.halfapi_conf = {
|
||||
'secret': 'testsecret',
|
||||
'production': False,
|
||||
'domain': {}
|
||||
}
|
||||
|
||||
self.halfapi_conf['domain'][self.domain_name] = {
|
||||
'name': self.domain_name,
|
||||
'router': self.router_path,
|
||||
'acl': self.acl_path,
|
||||
'module': self.module_name,
|
||||
'prefix': False,
|
||||
'enabled': True,
|
||||
'config': getattr(self, 'CONFIG', {})
|
||||
}
|
||||
|
||||
_, self.config_file = tempfile.mkstemp()
|
||||
with open(self.config_file, 'w') as fh:
|
||||
fh.write(toml.dumps(self.halfapi_conf))
|
||||
|
||||
self.halfapi = HalfAPI(self.halfapi_conf)
|
||||
|
||||
self.client = TestClient(self.halfapi.application)
|
||||
|
||||
self.module = importlib.import_module(
|
||||
self.module_name
|
||||
)
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def check_domain(self):
|
||||
result = None
|
||||
try:
|
||||
result = self.runner.invoke(cli, '--version')
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
result = self.runner.invoke(cli, ['domain', '--read', self.DOMAIN, self.config_file])
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
result_d = json.loads(result.stdout)
|
||||
result = self.runner.invoke(cli, ['run', '--help'])
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
# result = self.runner.invoke(cli, ['run', '--dryrun', self.DOMAIN])
|
||||
# self.assertEqual(result.exit_code, 0)
|
||||
except AssertionError as exc:
|
||||
print(f'Result {result}')
|
||||
print(f'Stdout {result.stdout}')
|
||||
print(f'Stderr {result.stderr}')
|
||||
raise exc
|
||||
except JSONDecodeError as exc:
|
||||
print(f'Result {result}')
|
||||
print(f'Stdout {result.stdout}')
|
||||
raise exc
|
||||
|
||||
|
||||
|
||||
return result_d
|
||||
|
||||
def check_routes(self):
|
||||
r = self.client.request('get', '/')
|
||||
assert r.status_code == 200
|
||||
schema = r.json()
|
||||
assert isinstance(schema, dict)
|
||||
assert 'openapi' in schema
|
||||
assert 'info' in schema
|
||||
assert 'paths' in schema
|
||||
|
||||
r = self.client.request('get', '/halfapi/acls')
|
||||
assert r.status_code == 200
|
||||
d_r = r.json()
|
||||
assert isinstance(d_r, dict)
|
||||
assert self.domain_name in d_r.keys()
|
||||
|
||||
ACLS = HalfDomain.acls(self.module, self.acl_path)
|
||||
assert len(ACLS) == len(d_r[self.domain_name])
|
||||
|
||||
for acl_rule in ACLS:
|
||||
assert len(acl_rule.name) > 0
|
||||
assert acl_rule.name in d_r[self.domain_name]
|
||||
assert len(acl_rule.documentation) > 0
|
||||
assert isinstance(acl_rule.priority, int)
|
||||
assert acl_rule.priority >= 0
|
||||
|
||||
if acl_rule.public is True:
|
||||
r = self.client.request('get', f'/halfapi/acls/{acl_rule.name}')
|
||||
assert r.status_code in [200, 401]
|
@ -1,3 +0,0 @@
|
||||
pyexcel>=0.6.3,<1
|
||||
pyexcel-ods>=0.5.6,<1
|
||||
pyexcel-xlsx=0.6.0,<1
|
@ -1,5 +1,5 @@
|
||||
[pytest]
|
||||
testpaths = tests halfapi
|
||||
testpaths = tests
|
||||
addopts = --doctest-modules
|
||||
doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL
|
||||
pythonpath = ./tests
|
||||
doctest_optionflags = ELLIPSIS
|
||||
python_paths = ./tests
|
||||
|
@ -1,61 +1,5 @@
|
||||
alog==0.9.13
|
||||
anyio==3.4.0
|
||||
asgiref==3.4.1
|
||||
astroid==2.9.0
|
||||
attrs==21.2.0
|
||||
bleach==4.1.0
|
||||
build==0.7.0
|
||||
certifi==2021.10.8
|
||||
cffi==1.15.0
|
||||
charset-normalizer==2.0.9
|
||||
click==7.1.2
|
||||
colorama==0.4.4
|
||||
contextlib2==21.6.0
|
||||
cryptography==36.0.1
|
||||
docutils==0.18.1
|
||||
h11==0.12.0
|
||||
idna==3.3
|
||||
importlib-metadata==4.8.2
|
||||
iniconfig==1.1.1
|
||||
isort==5.10.1
|
||||
jeepney==0.7.1
|
||||
keyring==23.4.0
|
||||
lazy-object-proxy==1.7.1
|
||||
mccabe==0.6.1
|
||||
orjson==3.6.5
|
||||
packaging==21.3
|
||||
pep517==0.12.0
|
||||
pkginfo==1.8.2
|
||||
platformdirs==2.4.0
|
||||
pluggy==1.0.0
|
||||
py==1.11.0
|
||||
pycparser==2.21
|
||||
pyflakes==2.4.0
|
||||
Pygments==2.10.0
|
||||
PyJWT==2.3.0
|
||||
pylint==2.12.2
|
||||
pyparsing==3.0.6
|
||||
pytest==6.2.5
|
||||
pytest-asyncio==0.16.0
|
||||
pytest-pythonpath==0.7.3
|
||||
PyYAML==5.4.1
|
||||
readme-renderer==32.0
|
||||
requests==2.26.0
|
||||
requests-toolbelt==0.9.1
|
||||
rfc3986==1.5.0
|
||||
schema==0.7.5
|
||||
SecretStorage==3.3.1
|
||||
six==1.16.0
|
||||
sniffio==1.2.0
|
||||
starlette==0.17.1
|
||||
timing-asgi==0.2.1
|
||||
toml==0.10.2
|
||||
tomli==2.0.0
|
||||
tqdm==4.62.3
|
||||
twine==3.7.1
|
||||
urllib3==1.26.7
|
||||
uvicorn==0.16.0
|
||||
vulture==2.3
|
||||
webencodings==0.5.1
|
||||
wrapt==1.13.3
|
||||
zipp==3.6.0
|
||||
click
|
||||
starlette
|
||||
uvicorn
|
||||
PyJWT
|
||||
pygit2==0.28.2
|
||||
|
32
setup.py
32
setup.py
@ -43,43 +43,29 @@ setup(
|
||||
packages=get_packages(module_name),
|
||||
python_requires=">=3.8",
|
||||
install_requires=[
|
||||
"PyJWT>=2.6.0,<2.7.0",
|
||||
"starlette>=0.33,<0.34",
|
||||
"click>=8,<9",
|
||||
"PyJWT>=2.0.1",
|
||||
"starlette>=0.16,<0.17",
|
||||
"click>=7.1,<8",
|
||||
"uvicorn>=0.13,<1",
|
||||
"orjson>=3.8.5,<4",
|
||||
"pyyaml>=6,<7",
|
||||
"orjson>=3.4.7,<4",
|
||||
"pyyaml>=5.3.1,<6",
|
||||
"timing-asgi>=0.2.1,<1",
|
||||
"schema>=0.7.4,<1",
|
||||
"toml>=0.10,<0.11",
|
||||
"packaging>=19.0",
|
||||
"python-multipart"
|
||||
"schema>=0.7.4,<1"
|
||||
],
|
||||
classifiers=[
|
||||
"Development Status :: 3 - Alpha",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10"
|
||||
],
|
||||
extras_require={
|
||||
"tests":[
|
||||
"pytest>=7,<8",
|
||||
"pytest-asyncio",
|
||||
"pylint",
|
||||
"pytest",
|
||||
"requests",
|
||||
"httpx",
|
||||
"openapi-schema-validator",
|
||||
"openapi-spec-validator",
|
||||
"coverage"
|
||||
],
|
||||
"pyexcel":[
|
||||
"pyexcel",
|
||||
"pyexcel-ods3",
|
||||
"pyexcel-xlsx"
|
||||
"pytest-asyncio",
|
||||
"pylint"
|
||||
]
|
||||
},
|
||||
entry_points={
|
||||
|
@ -7,3 +7,4 @@ def test_config(cli_runner):
|
||||
cp = ConfigParser()
|
||||
cp.read_string(result.output)
|
||||
assert cp.has_section('project')
|
||||
assert cp.has_section('domain')
|
||||
|
@ -1,84 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import subprocess
|
||||
import importlib
|
||||
import tempfile
|
||||
from unittest.mock import patch
|
||||
import json
|
||||
import toml
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
from configparser import ConfigParser
|
||||
from halfapi.conf import DEFAULT_CONF, PROJECT_LEVEL_KEYS, DOMAIN_LEVEL_KEYS
|
||||
|
||||
PROJNAME = os.environ.get('PROJ','tmp_api')
|
||||
|
||||
|
||||
class TestCliProj():
|
||||
def test_cmds(self, project_runner):
|
||||
assert project_runner('--help').exit_code == 0
|
||||
#assert project_runner('run', '--help').exit_code == 0
|
||||
#assert project_runner('domain', '--help').exit_code == 0
|
||||
|
||||
|
||||
def test_domain_commands(self, project_runner):
|
||||
""" TODO: Test create command
|
||||
"""
|
||||
test_conf = {
|
||||
'project': {
|
||||
'port': '3010',
|
||||
'loglevel': 'warning'
|
||||
},
|
||||
'domain': {
|
||||
'dummy_domain': {
|
||||
'port': 4242,
|
||||
'name': 'dummy_domain',
|
||||
'enabled': True
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r = project_runner('domain')
|
||||
print(r.stdout)
|
||||
assert r.exit_code == 1
|
||||
_, tmp_conf = tempfile.mkstemp()
|
||||
with open(tmp_conf, 'w') as fh:
|
||||
fh.write(
|
||||
toml.dumps(test_conf)
|
||||
)
|
||||
|
||||
r = project_runner(f'domain dummy_domain --conftest {tmp_conf}')
|
||||
assert r.exit_code == 0
|
||||
r_conf = toml.loads(r.stdout)
|
||||
for key, value in r_conf.items():
|
||||
if key == 'domain':
|
||||
continue
|
||||
assert key in PROJECT_LEVEL_KEYS
|
||||
if key == 'port':
|
||||
assert value == test_conf['domain']['dummy_domain']['port']
|
||||
elif key == 'loglevel':
|
||||
assert value == test_conf['project']['loglevel']
|
||||
else:
|
||||
assert value == DEFAULT_CONF[key.upper()]
|
||||
|
||||
|
||||
assert json.dumps(test_conf['domain']) == json.dumps(r_conf['domain'])
|
||||
|
||||
for key in test_conf['domain']['dummy_domain'].keys():
|
||||
assert key in DOMAIN_LEVEL_KEYS
|
||||
|
||||
# Default command "run"
|
||||
r = project_runner(f'domain dummy_domain --dry-run {tmp_conf}')
|
||||
print(r.stdout)
|
||||
assert r.exit_code == 0
|
||||
|
||||
|
||||
def test_config_commands(self, project_runner):
|
||||
try:
|
||||
r = project_runner('config')
|
||||
assert r.exit_code == 0
|
||||
except AssertionError as exc:
|
||||
subprocess.call(['tree', '-a'])
|
||||
raise exc
|
||||
|
@ -1,23 +1,13 @@
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
from halfapi.cli.cli import cli
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
def test_run_noproject(cli_runner):
|
||||
with cli_runner.isolated_filesystem():
|
||||
result = cli_runner.invoke(cli, ['config'])
|
||||
print(result.stdout)
|
||||
assert result.exit_code == 0
|
||||
|
||||
result = cli_runner.invoke(cli, ['run', '--dryrun'])
|
||||
try:
|
||||
assert result.exit_code == 0
|
||||
except AssertionError as exc:
|
||||
print(result.stdout)
|
||||
raise exc
|
||||
result = cli_runner.invoke(cli, ['run'])
|
||||
assert result.exit_code == 1
|
||||
|
||||
"""
|
||||
def test_run_empty_project(cli_runner):
|
||||
|
@ -20,7 +20,7 @@ from starlette.testclient import TestClient
|
||||
from halfapi import __version__
|
||||
from halfapi.halfapi import HalfAPI
|
||||
from halfapi.cli.cli import cli
|
||||
from halfapi.cli.init import init
|
||||
from halfapi.cli.init import init, format_halfapi_etc
|
||||
from halfapi.cli.domain import domain, create_domain
|
||||
from halfapi.lib.responses import ORJSONResponse
|
||||
from halfapi.lib.jwt_middleware import JWTAuthenticationBackend
|
||||
@ -39,12 +39,7 @@ from halfapi.lib.jwt_middleware import (
|
||||
def dummy_domain():
|
||||
yield {
|
||||
'name': 'dummy_domain',
|
||||
'router': '.routers',
|
||||
'enabled': True,
|
||||
'prefix': False,
|
||||
'config': {
|
||||
'test': True
|
||||
}
|
||||
'router': 'dummy_domain.routers'
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
@ -180,12 +175,10 @@ def project_runner(runner, halfapicli, tree):
|
||||
with open(SECRET_PATH, 'w') as f:
|
||||
f.write(str(uuid1()))
|
||||
|
||||
"""
|
||||
with open(os.path.join('.halfapi', PROJNAME), 'w') as halfapi_etc:
|
||||
PROJ_CONFIG = re.sub('secret = .*', f'secret = {SECRET_PATH}',
|
||||
format_halfapi_etc(PROJNAME, os.getcwd()))
|
||||
halfapi_etc.write(PROJ_CONFIG)
|
||||
"""
|
||||
|
||||
|
||||
###
|
||||
@ -260,10 +253,10 @@ def dummy_project():
|
||||
f'secret = {halfapi_secret}\n',
|
||||
'port = 3050\n',
|
||||
'loglevel = debug\n',
|
||||
'[domain.dummy_domain]\n',
|
||||
'[domain]\n',
|
||||
f'name = {domain}\n',
|
||||
'router = dummy_domain.routers\n',
|
||||
f'[domain.dummy_domain.config]\n',
|
||||
f'[{domain}]\n',
|
||||
'test = True'
|
||||
])
|
||||
|
||||
@ -278,16 +271,12 @@ def application_debug(project_runner):
|
||||
'secret':'turlututu',
|
||||
'production':False,
|
||||
'domain': {
|
||||
'dummy_domain': {
|
||||
'name': 'dummy_domain',
|
||||
'router': '.routers',
|
||||
'enabled': True,
|
||||
'prefix': False,
|
||||
'config':{
|
||||
'test': True
|
||||
}
|
||||
}
|
||||
'name': 'test_domain',
|
||||
'router': 'test_domain.routers'
|
||||
},
|
||||
'config':{
|
||||
'domain_config': {'test_domain': {'test': True}}
|
||||
}
|
||||
})
|
||||
|
||||
assert isinstance(halfAPI, HalfAPI)
|
||||
@ -299,13 +288,9 @@ def application_domain(dummy_domain):
|
||||
return HalfAPI({
|
||||
'secret':'turlututu',
|
||||
'production':True,
|
||||
'domain': {
|
||||
'dummy_domain': {
|
||||
**dummy_domain,
|
||||
'domain': dummy_domain,
|
||||
'config':{
|
||||
'test': True
|
||||
}
|
||||
}
|
||||
'domain_config': {'dummy_domain': {'test': True}}
|
||||
}
|
||||
}).application
|
||||
|
||||
|
@ -1,27 +1,5 @@
|
||||
from halfapi import __version__ as halfapi_version
|
||||
__name__ = 'dummy_domain'
|
||||
__version__ = '0.0.0'
|
||||
__patch_release__ = '0.0.0'
|
||||
__routers__ = '.routers'
|
||||
|
||||
domain = {
|
||||
'name': 'dummy_domain',
|
||||
'version': '0.0.0',
|
||||
'id': '8b88e60a625369235b36c2d6d70756a0c02c1c7fb169fcee6dc820bcf9723f5a',
|
||||
'deps': (
|
||||
('halfapi', '=={}'.format(halfapi_version)),
|
||||
),
|
||||
'schema_components': {
|
||||
'schemas': {
|
||||
'Pinnochio': {
|
||||
'type': 'object',
|
||||
'required': {
|
||||
'id',
|
||||
'name',
|
||||
'nose_size'
|
||||
},
|
||||
'properties': {
|
||||
'id': {'type': 'string'},
|
||||
'name': {'type': 'string'},
|
||||
'nose_size': {'type': 'number'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,21 @@
|
||||
from halfapi.lib import acl
|
||||
from halfapi.lib.acl import public, private, ACLS
|
||||
from halfapi.lib.acl import public
|
||||
from random import randint
|
||||
|
||||
def random(*args):
|
||||
def random():
|
||||
""" Random access ACL
|
||||
"""
|
||||
return randint(0,1) == 1
|
||||
|
||||
def denied():
|
||||
""" Access denied
|
||||
"""
|
||||
return False
|
||||
|
||||
ACLS = (
|
||||
*ACLS,
|
||||
('random', random.__doc__, 10)
|
||||
('public', public.__doc__, 999),
|
||||
('random', random.__doc__, 10),
|
||||
('denied', denied.__doc__, 0)
|
||||
)
|
||||
|
||||
|
||||
|
@ -12,9 +12,6 @@ async def get(test):
|
||||
"""
|
||||
description:
|
||||
returns the path parameter
|
||||
responses:
|
||||
200:
|
||||
description: test response
|
||||
"""
|
||||
return ORJSONResponse(str(test))
|
||||
|
||||
@ -22,9 +19,6 @@ def post(test):
|
||||
"""
|
||||
description:
|
||||
returns the path parameter
|
||||
responses:
|
||||
200:
|
||||
description: test response
|
||||
"""
|
||||
return str(test)
|
||||
|
||||
@ -32,9 +26,6 @@ def patch(test):
|
||||
"""
|
||||
description:
|
||||
returns the path parameter
|
||||
responses:
|
||||
200:
|
||||
description: test response
|
||||
"""
|
||||
return str(test)
|
||||
|
||||
@ -42,9 +33,6 @@ def put(test):
|
||||
"""
|
||||
description:
|
||||
returns the path parameter
|
||||
responses:
|
||||
200:
|
||||
description: test response
|
||||
"""
|
||||
return str(test)
|
||||
|
||||
@ -52,8 +40,5 @@ def delete(test):
|
||||
"""
|
||||
description:
|
||||
returns the path parameter
|
||||
responses:
|
||||
200:
|
||||
description: test response
|
||||
"""
|
||||
return str(test)
|
||||
|
@ -1,21 +1,10 @@
|
||||
from uuid import uuid4
|
||||
from halfapi.lib import acl
|
||||
ACLS = {
|
||||
'GET' : [{'acl':acl.public}]
|
||||
}
|
||||
def get():
|
||||
"""
|
||||
description: The pinnochio guy
|
||||
responses:
|
||||
200:
|
||||
description: test response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Pinnochio"
|
||||
description:
|
||||
Not implemented
|
||||
"""
|
||||
return {
|
||||
'id': str(uuid4()),
|
||||
'name': 'pinnochio',
|
||||
'nose_size': 42
|
||||
}
|
||||
raise NotImplementedError
|
||||
|
@ -26,54 +26,13 @@ ACLS = {
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
'POST' : [
|
||||
{
|
||||
'acl':acl.private,
|
||||
'args': {
|
||||
'required': {
|
||||
'foo', 'bar'
|
||||
},
|
||||
'optional': {
|
||||
'x'
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
{
|
||||
'acl':acl.public,
|
||||
'args': {
|
||||
'required': {
|
||||
'foo', 'baz'
|
||||
},
|
||||
'optional': {
|
||||
'truebidoo',
|
||||
'z'
|
||||
}
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
def get(data):
|
||||
def get(halfapi, data):
|
||||
"""
|
||||
description:
|
||||
returns the arguments passed in
|
||||
responses:
|
||||
200:
|
||||
description: test response
|
||||
returns the configuration of the domain
|
||||
"""
|
||||
logger.error('%s', data['foo'])
|
||||
return data
|
||||
|
||||
def post(data):
|
||||
"""
|
||||
description:
|
||||
returns the arguments passed in
|
||||
responses:
|
||||
200:
|
||||
description: test response
|
||||
"""
|
||||
logger.error('%s', data)
|
||||
return data
|
||||
return {'foo': data['foo'], 'bar': data['bar']}
|
||||
|
@ -25,43 +25,24 @@ ROUTES = {
|
||||
async def get_abc_alphabet_TEST(request, *args, **kwargs):
|
||||
"""
|
||||
description: Not implemented
|
||||
responses:
|
||||
200:
|
||||
description: test response
|
||||
parameters:
|
||||
- name: test
|
||||
in: path
|
||||
description: Test parameter in route with "ROUTES" constant
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
"""
|
||||
return NotImplementedResponse()
|
||||
|
||||
async def get_abc_pinnochio(request, *args, **kwargs):
|
||||
"""
|
||||
description: Not implemented
|
||||
responses:
|
||||
200:
|
||||
description: test response
|
||||
"""
|
||||
return NotImplementedResponse()
|
||||
|
||||
async def get_config(request, *args, **kwargs):
|
||||
"""
|
||||
description: Not implemented
|
||||
responses:
|
||||
200:
|
||||
description: test response
|
||||
"""
|
||||
return NotImplementedResponse()
|
||||
|
||||
async def get_arguments(request, *args, **kwargs):
|
||||
"""
|
||||
description: Liste des datatypes.
|
||||
responses:
|
||||
200:
|
||||
description: test response
|
||||
"""
|
||||
return ORJSONResponse({
|
||||
'foo': kwargs.get('data').get('foo'),
|
@ -12,22 +12,6 @@ def get(halfapi):
|
||||
"""
|
||||
description:
|
||||
returns the configuration of the domain
|
||||
responses:
|
||||
200:
|
||||
description: test response
|
||||
"""
|
||||
logger.error('%s', halfapi)
|
||||
# TODO: Remove in 0.7.0
|
||||
try:
|
||||
assert 'test' in halfapi['config']['domain']['dummy_domain']['config']
|
||||
except AssertionError as exc:
|
||||
logger.error('No TEST in halfapi[config][domain][dummy_domain][config]')
|
||||
raise exc
|
||||
|
||||
try:
|
||||
assert 'test' in halfapi['config']
|
||||
except AssertionError as exc:
|
||||
logger.error('No TEST in halfapi[config]')
|
||||
raise exc
|
||||
|
||||
return halfapi['config']
|
||||
|
@ -1,8 +0,0 @@
|
||||
param_docstring = """
|
||||
name: second
|
||||
in: path
|
||||
description: second parameter description test
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
"""
|
@ -1,20 +0,0 @@
|
||||
from uuid import UUID
|
||||
from halfapi.lib import acl
|
||||
ACLS = {
|
||||
'GET': [{'acl': acl.public}]
|
||||
}
|
||||
|
||||
def get(first, second, third):
|
||||
"""
|
||||
description: a Test route for path parameters
|
||||
responses:
|
||||
200:
|
||||
description: The test passed!
|
||||
500:
|
||||
description: The test did not pass :(
|
||||
"""
|
||||
assert isintance(first, str)
|
||||
assert isintance(second, UUID)
|
||||
assert isintance(third, int)
|
||||
|
||||
return ''
|
@ -1,13 +0,0 @@
|
||||
from halfapi.lib import acl
|
||||
|
||||
ACLS = {
|
||||
'GET': [{'acl':acl.public}]
|
||||
}
|
||||
|
||||
def get(ext, ret_type='html'):
|
||||
"""
|
||||
responses:
|
||||
200:
|
||||
description: dummy abc.alphabet route
|
||||
"""
|
||||
return '\n'.join(('trololo', '', 'ololotr'))
|
@ -1,31 +0,0 @@
|
||||
from halfapi.lib import acl
|
||||
|
||||
ACLS = {
|
||||
'GET': [{'acl':acl.public}],
|
||||
'POST': [
|
||||
{
|
||||
'acl':acl.public,
|
||||
'args': {
|
||||
'required': {'trou'},
|
||||
'optional': {'troo'}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def get(ext, halfapi={}, ret_type='html'):
|
||||
"""
|
||||
responses:
|
||||
200:
|
||||
description: dummy abc.alphabet route
|
||||
"""
|
||||
return '\n'.join(('trololo', '', 'ololotr'))
|
||||
|
||||
def post(ext, data={'troo': 'fidget'}, halfapi={}, ret_type='html'):
|
||||
"""
|
||||
responses:
|
||||
200:
|
||||
description: dummy abc.alphabet route
|
||||
"""
|
||||
print(data)
|
||||
return '\n'.join(('trololo', '', 'ololotr'))
|
@ -1,13 +0,0 @@
|
||||
from halfapi.lib import acl
|
||||
|
||||
ACLS = {
|
||||
'GET': [{'acl':acl.public}]
|
||||
}
|
||||
|
||||
def get(ret_type='html'):
|
||||
"""
|
||||
responses:
|
||||
200:
|
||||
description: dummy abc.alphabet route
|
||||
"""
|
||||
return '\n'.join(('trololo', '', 'ololotr'))
|
@ -1,4 +0,0 @@
|
||||
from . import get
|
||||
|
||||
def test_get():
|
||||
assert isinstance(get(), str)
|
@ -17,7 +17,7 @@ def test_acl_Check(dummy_app, token_debug_false_builder):
|
||||
dummy_app.add_route('/test_public', test_route_public)
|
||||
test_client = TestClient(dummy_app)
|
||||
|
||||
resp = test_client.request('get', '/test_public?check')
|
||||
resp = test_client.get('/test_public?check')
|
||||
assert resp.status_code == 200
|
||||
|
||||
@HalfRoute.acl_decorator(params=[{'acl':acl.private}])
|
||||
@ -28,10 +28,10 @@ def test_acl_Check(dummy_app, token_debug_false_builder):
|
||||
dummy_app.add_route('/test_private', test_route_private)
|
||||
test_client = TestClient(dummy_app)
|
||||
|
||||
resp = test_client.request('get', '/test_private')
|
||||
resp = test_client.get('/test_private')
|
||||
assert resp.status_code == 401
|
||||
|
||||
resp = test_client.request('get', '/test_private?check')
|
||||
resp = test_client.get('/test_private?check')
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
|
@ -8,8 +8,6 @@ from halfapi.lib.domain import NoDomainsException
|
||||
def test_halfapi_dummy_domain(dummy_domain):
|
||||
with patch('starlette.applications.Starlette') as mock:
|
||||
mock.return_value = MagicMock()
|
||||
config = {}
|
||||
config['domain'] = {}
|
||||
config['domain'][dummy_domain['name']] = dummy_domain
|
||||
print(config)
|
||||
halfapi = HalfAPI(config)
|
||||
halfapi = HalfAPI({
|
||||
'domain': dummy_domain
|
||||
})
|
||||
|
@ -36,7 +36,6 @@ def test_options(runner):
|
||||
assert r.exit_code == 0
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
def test_init_project_fail(runner):
|
||||
# Missing argument (project)
|
||||
testproject = 'testproject'
|
||||
@ -60,7 +59,6 @@ def test_init_project_fail(runner):
|
||||
r = runner.invoke(Cli, ['init', testproject])
|
||||
assert r.exit_code == 1
|
||||
|
||||
@pytest.mark.skip
|
||||
def test_init_project(runner):
|
||||
"""
|
||||
"""
|
||||
@ -83,8 +81,7 @@ def test_init_project(runner):
|
||||
assert cp.has_option('project', 'name')
|
||||
assert cp.get('project', 'name') == PROJNAME
|
||||
assert cp.get('project', 'halfapi_version') == __version__
|
||||
# removal of domain section (0.6)
|
||||
# assert cp.has_section('domain')
|
||||
assert cp.has_section('domain')
|
||||
except AssertionError as exc:
|
||||
subprocess.run(['tree', '-a', os.getcwd()])
|
||||
raise exc
|
40
tests/test_cli_proj.py
Normal file
40
tests/test_cli_proj.py
Normal file
@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import subprocess
|
||||
import importlib
|
||||
import tempfile
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
from configparser import ConfigParser
|
||||
|
||||
PROJNAME = os.environ.get('PROJ','tmp_api')
|
||||
|
||||
|
||||
class TestCliProj():
|
||||
def test_cmds(self, project_runner):
|
||||
assert project_runner('--help').exit_code == 0
|
||||
#assert project_runner('run', '--help').exit_code == 0
|
||||
#assert project_runner('domain', '--help').exit_code == 0
|
||||
|
||||
|
||||
def test_domain_commands(self, project_runner):
|
||||
""" TODO: Test create command
|
||||
"""
|
||||
r = project_runner('domain')
|
||||
print(r.stdout)
|
||||
assert r.exit_code == 1
|
||||
r = project_runner('domain dummy_domain')
|
||||
print(r.stdout)
|
||||
assert r.exit_code == 0
|
||||
|
||||
|
||||
def test_config_commands(self, project_runner):
|
||||
try:
|
||||
r = project_runner('config')
|
||||
assert r.exit_code == 0
|
||||
except AssertionError as exc:
|
||||
subprocess.call(['tree', '-a'])
|
||||
raise exc
|
||||
|
@ -7,12 +7,8 @@ class TestConf(TestCase):
|
||||
def setUp(self):
|
||||
self.args = {
|
||||
'domain': {
|
||||
'dummy_domain': {
|
||||
'name': 'dummy_domain',
|
||||
'router': '.routers',
|
||||
'enabled': True,
|
||||
'prefix': False,
|
||||
}
|
||||
'router': 'dummy_domain.routers'
|
||||
}
|
||||
}
|
||||
def tearDown(self):
|
||||
@ -42,11 +38,20 @@ class TestConf(TestCase):
|
||||
from halfapi.conf import (
|
||||
CONFIG,
|
||||
SCHEMA,
|
||||
SECRET,
|
||||
DOMAINSDICT,
|
||||
PROJECT_NAME,
|
||||
HOST,
|
||||
PORT,
|
||||
CONF_DIR
|
||||
)
|
||||
|
||||
assert isinstance(CONFIG, dict)
|
||||
assert isinstance(CONFIG.get('project_name'), str)
|
||||
assert isinstance(SCHEMA, dict)
|
||||
assert isinstance(CONFIG.get('secret'), str)
|
||||
assert isinstance(CONFIG.get('host'), str)
|
||||
assert isinstance(CONFIG.get('port'), int)
|
||||
assert isinstance(SECRET, str)
|
||||
assert isinstance(DOMAINSDICT(), dict)
|
||||
assert isinstance(PROJECT_NAME, str)
|
||||
assert isinstance(HOST, str)
|
||||
assert isinstance(PORT, str)
|
||||
assert str(int(PORT)) == PORT
|
||||
assert isinstance(CONF_DIR, str)
|
||||
|
@ -6,62 +6,34 @@ import subprocess
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import pprint
|
||||
import openapi_spec_validator
|
||||
import logging
|
||||
logger = logging.getLogger()
|
||||
|
||||
from halfapi.lib.constants import API_SCHEMA
|
||||
|
||||
|
||||
def test_halfapi_whoami(application_debug):
|
||||
def test_routes(application_debug):
|
||||
# @TODO : If we use isolated filesystem multiple times that creates a bug.
|
||||
# So we use a single function with fixture "application debug"
|
||||
|
||||
c = TestClient(application_debug)
|
||||
r = c.request('get', '/halfapi/whoami')
|
||||
r = c.get('/halfapi/whoami')
|
||||
assert r.status_code == 200
|
||||
|
||||
def test_halfapi_log(application_debug):
|
||||
# @TODO : If we use isolated filesystem multiple times that creates a bug.
|
||||
# So we use a single function with fixture "application debug"
|
||||
|
||||
c = TestClient(application_debug)
|
||||
|
||||
r = c.request('get', '/halfapi/log')
|
||||
r = c.get('/halfapi/log')
|
||||
assert r.status_code == 200
|
||||
|
||||
def test_halfapi_error_400(application_debug):
|
||||
# @TODO : If we use isolated filesystem multiple times that creates a bug.
|
||||
# So we use a single function with fixture "application debug"
|
||||
|
||||
c = TestClient(application_debug)
|
||||
|
||||
r = c.request('get', '/halfapi/error/400')
|
||||
r = c.get('/halfapi/error/400')
|
||||
assert r.status_code == 400
|
||||
|
||||
def test_halfapi_error_404(application_debug):
|
||||
# @TODO : If we use isolated filesystem multiple times that creates a bug.
|
||||
# So we use a single function with fixture "application debug"
|
||||
|
||||
c = TestClient(application_debug)
|
||||
|
||||
r = c.request('get', '/halfapi/error/404')
|
||||
r = c.get('/halfapi/error/404')
|
||||
assert r.status_code == 404
|
||||
|
||||
def test_halfapi_error_500(application_debug):
|
||||
# @TODO : If we use isolated filesystem multiple times that creates a bug.
|
||||
# So we use a single function with fixture "application debug"
|
||||
|
||||
c = TestClient(application_debug)
|
||||
|
||||
r = c.request('get', '/halfapi/error/500')
|
||||
r = c.get('/halfapi/error/500')
|
||||
assert r.status_code == 500
|
||||
r = c.get('/')
|
||||
d_r = r.json()
|
||||
assert isinstance(d_r, dict)
|
||||
assert API_SCHEMA.validate(d_r)
|
||||
|
||||
def test_schema(application_debug):
|
||||
c = TestClient(application_debug)
|
||||
|
||||
r = c.request('get', '/')
|
||||
schema = r.json()
|
||||
assert isinstance(schema, dict)
|
||||
openapi_spec_validator.validate_spec(schema)
|
||||
"""
|
||||
TODO: Find a way to test exception raising
|
||||
try:
|
||||
r = c.get('/halfapi/exception')
|
||||
assert r.status_code == 500
|
||||
except Exception:
|
||||
print('exception')
|
||||
"""
|
||||
|
@ -1,106 +1,54 @@
|
||||
import pytest
|
||||
from halfapi.testing.test_domain import TestDomain
|
||||
import importlib
|
||||
import functools
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
from unittest import TestCase
|
||||
from click.testing import CliRunner
|
||||
from halfapi.cli.cli import cli
|
||||
from pprint import pprint
|
||||
import logging
|
||||
logger = logging.getLogger()
|
||||
|
||||
class TestDummyDomain(TestDomain):
|
||||
from .dummy_domain import domain
|
||||
__name__ = domain.get('name')
|
||||
__routers__ = domain.get('routers')
|
||||
class TestDomain(TestCase):
|
||||
DOMAIN = 'dummy_domain'
|
||||
ROUTERS = 'routers'
|
||||
|
||||
DOMAIN = __name__
|
||||
CONFIG = {'test': True}
|
||||
@property
|
||||
def router_module(self):
|
||||
return '.'.join((self.DOMAIN, self.ROUTERS))
|
||||
|
||||
def test_domain(self):
|
||||
self.check_domain()
|
||||
def setUp(self):
|
||||
class_ = CliRunner
|
||||
def invoke_wrapper(f):
|
||||
"""Augment CliRunner.invoke to emit its output to stdout.
|
||||
|
||||
This enables pytest to show the output in its logs on test
|
||||
failures.
|
||||
|
||||
"""
|
||||
@functools.wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
echo = kwargs.pop('echo', False)
|
||||
result = f(*args, **kwargs)
|
||||
|
||||
if echo is True:
|
||||
sys.stdout.write(result.output)
|
||||
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
class_.invoke = invoke_wrapper(class_.invoke)
|
||||
self.runner = class_()
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_routes(self):
|
||||
self.check_routes()
|
||||
|
||||
def test_html_route(self):
|
||||
res = self.client.request('get', '/ret_type')
|
||||
assert res.status_code == 200
|
||||
assert isinstance(res.content.decode(), str)
|
||||
assert res.headers['content-type'].split(';')[0] == 'text/html'
|
||||
|
||||
res = self.client.request('get', '/ret_type/h24')
|
||||
assert res.status_code == 200
|
||||
assert isinstance(res.content.decode(), str)
|
||||
assert res.headers['content-type'].split(';')[0] == 'text/html'
|
||||
|
||||
res = self.client.request('get', '/ret_type/h24/config')
|
||||
assert res.status_code == 200
|
||||
assert isinstance(res.content.decode(), str)
|
||||
assert res.headers['content-type'].split(';')[0] == 'text/html'
|
||||
|
||||
res = self.client.request('post', '/ret_type/h24/config', json={
|
||||
'trou': 'glet'
|
||||
})
|
||||
assert res.status_code == 200
|
||||
assert isinstance(res.content.decode(), str)
|
||||
assert res.headers['content-type'].split(';')[0] == 'text/html'
|
||||
|
||||
def test_arguments__get_routes(self):
|
||||
res = self.client.request('post', '/arguments?foo=1&x=3')
|
||||
|
||||
assert res.status_code == 400
|
||||
|
||||
arg_dict = {'foo': '1', 'bar': '2', 'x': '3'}
|
||||
res = self.client.request('get', '/arguments?foo=1&bar=2&x=3')
|
||||
assert res.json() == arg_dict
|
||||
|
||||
res = self.client.request('get', '/arguments?foo=1&bar=2&x=3&y=4')
|
||||
assert res.json() == arg_dict
|
||||
|
||||
def test_arguments_post_routes(self):
|
||||
arg_dict = {}
|
||||
res = self.client.request('post', '/arguments', json=arg_dict)
|
||||
|
||||
assert res.status_code == 400
|
||||
|
||||
arg_dict = {'foo': '1', 'bar': '3'}
|
||||
res = self.client.request('post', '/arguments', json=arg_dict)
|
||||
|
||||
assert res.status_code == 400
|
||||
|
||||
arg_dict = {'foo': '1', 'baz': '3'}
|
||||
res = self.client.request('post', '/arguments', json=arg_dict)
|
||||
|
||||
assert res.json() == arg_dict
|
||||
|
||||
arg_dict = {'foo': '1', 'baz': '3', 'truebidoo': '4'}
|
||||
res = self.client.request('post', '/arguments', json=arg_dict)
|
||||
|
||||
assert res.json() == arg_dict
|
||||
|
||||
res = self.client.request('post', '/arguments', json={ **arg_dict, 'y': '4'})
|
||||
assert res.json() == arg_dict
|
||||
|
||||
res = self.client.request('post', '/arguments', json={ **arg_dict, 'z': True})
|
||||
assert res.json() == {**arg_dict, 'z': True}
|
||||
|
||||
def test_schema_path_params(self):
|
||||
res = self.client.request('get', '/halfapi/schema')
|
||||
schema = res.json()
|
||||
|
||||
logger.debug(schema)
|
||||
|
||||
assert len(schema['paths']) > 0
|
||||
route = schema['paths']['/path_params/{first}/one/{second}/two/{third}']
|
||||
|
||||
assert 'parameters' in route['get']
|
||||
parameters = route['get']['parameters']
|
||||
|
||||
assert len(parameters) == 3
|
||||
|
||||
param_map = {
|
||||
elt['name']: elt
|
||||
for elt in parameters
|
||||
}
|
||||
|
||||
assert param_map['second']['description'] == 'second parameter description test'
|
||||
|
||||
|
||||
|
||||
|
||||
result = self.runner.invoke(cli, '--version')
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
result = self.runner.invoke(cli, ['routes', '--export', self.router_module])
|
||||
self.assertEqual(result.exit_code, 0)
|
||||
print(result.stdout)
|
||||
# result_d = json.loads(result.stdout)
|
||||
# self.assertTrue()
|
||||
|
@ -1,12 +1,11 @@
|
||||
import importlib
|
||||
from halfapi.testing.test_domain import TestDomain
|
||||
|
||||
def test_dummy_domain():
|
||||
from . import dummy_domain
|
||||
from .dummy_domain import acl
|
||||
assert acl.public() is True
|
||||
assert isinstance(acl.random(), int)
|
||||
assert acl.private() is False
|
||||
assert acl.denied() is False
|
||||
|
||||
|
||||
from .dummy_domain import routers
|
||||
@ -14,6 +13,7 @@ def test_dummy_domain():
|
||||
from .dummy_domain.routers.abc.alphabet.TEST_uuid import get
|
||||
from .dummy_domain.routers.abc.pinnochio import get
|
||||
from .dummy_domain.routers.config import get
|
||||
from .dummy_domain.routers.config import get
|
||||
from .dummy_domain.routers import async_router
|
||||
from .dummy_domain.routers.async_router import ROUTES, get_abc_alphabet_TEST, get_abc_pinnochio, get_config, get_arguments
|
||||
async_mod = importlib.import_module('dummy_domain.routers.async', '.')
|
||||
fcts = ['get_abc_alphabet_TEST', 'get_abc_pinnochio', 'get_config', 'get_arguments']
|
||||
for fct in fcts:
|
||||
getattr(async_mod, fct)
|
||||
|
@ -4,17 +4,15 @@ import importlib
|
||||
import subprocess
|
||||
import time
|
||||
import pytest
|
||||
import json
|
||||
from pprint import pprint
|
||||
from starlette.routing import Route
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from halfapi.lib.domain import gen_router_routes
|
||||
|
||||
def test_get_config_route(dummy_project, application_domain):
|
||||
c = TestClient(application_domain)
|
||||
r = c.request('get', '/')
|
||||
assert r.status_code == 200
|
||||
pprint(r.json())
|
||||
r = c.request('get', '/config')
|
||||
r = c.get('/config')
|
||||
assert r.status_code == 200
|
||||
pprint(r.json())
|
||||
assert 'test' in r.json()
|
||||
@ -25,27 +23,27 @@ def test_get_route(dummy_project, application_domain):
|
||||
dummy_domain_routes = [
|
||||
('config','GET'),
|
||||
('config','GET'),
|
||||
('async_router/abc/pinnochio','GET'),
|
||||
('async_router/config','GET'),
|
||||
('async/abc/pinnochio','GET'),
|
||||
('async/config','GET'),
|
||||
# ('abc/pinnochio','GET'),
|
||||
# ('abc/alphabet','GET'),
|
||||
]
|
||||
|
||||
for route_def in dummy_domain_routes:
|
||||
for route_def in []:#dummy_domain_routes:
|
||||
path, verb = route_def[0], route_def[1]
|
||||
route_path = '/{}'.format(path)
|
||||
print(route_path)
|
||||
try:
|
||||
if verb.lower() == 'get':
|
||||
r = c.request('get', route_path)
|
||||
r = c.get(route_path)
|
||||
elif verb.lower() == 'post':
|
||||
r = c.request('post', route_path)
|
||||
r = c.post(route_path)
|
||||
elif verb.lower() == 'patch':
|
||||
r = c.request('patch', route_path)
|
||||
r = c.patch(route_path)
|
||||
elif verb.lower() == 'put':
|
||||
r = c.request('put', route_path)
|
||||
r = c.put(route_path)
|
||||
elif verb.lower() == 'delete':
|
||||
r = c.request('delete', route_path)
|
||||
r = c.delete(route_path)
|
||||
else:
|
||||
raise Exception(verb)
|
||||
try:
|
||||
@ -70,7 +68,7 @@ def test_get_route(dummy_project, application_domain):
|
||||
path = path.format(test=str(test_uuid))
|
||||
route_path = f'/{path}'
|
||||
if verb.lower() == 'get':
|
||||
r = c.request('get', f'{route_path}')
|
||||
r = c.get(f'{route_path}')
|
||||
|
||||
assert r.status_code == 200
|
||||
|
||||
@ -79,7 +77,7 @@ def test_delete_route(dummy_project, application_domain):
|
||||
c = TestClient(application_domain)
|
||||
from uuid import uuid4
|
||||
arg = str(uuid4())
|
||||
r = c.request('delete', f'/abc/alphabet/{arg}')
|
||||
r = c.delete(f'/abc/alphabet/{arg}')
|
||||
assert r.status_code == 200
|
||||
assert isinstance(r.json(), str)
|
||||
|
||||
@ -87,23 +85,23 @@ def test_arguments_route(dummy_project, application_domain):
|
||||
c = TestClient(application_domain)
|
||||
|
||||
path = '/arguments'
|
||||
r = c.request('get', path)
|
||||
r = c.get(path)
|
||||
assert r.status_code == 400
|
||||
r = c.request('get', path, params={'foo':True})
|
||||
r = c.get(path, params={'foo':True})
|
||||
assert r.status_code == 400
|
||||
arg = {'foo':True, 'bar':True}
|
||||
r = c.request('get', path, params=arg)
|
||||
r = c.get(path, params=arg)
|
||||
assert r.status_code == 200
|
||||
for key, val in arg.items():
|
||||
assert json.loads(r.json()[key]) == val
|
||||
path = '/async_router/arguments'
|
||||
r = c.request('get', path)
|
||||
assert r.json()[key] == str(val)
|
||||
path = '/async/arguments'
|
||||
r = c.get(path)
|
||||
assert r.status_code == 400
|
||||
r = c.request('get', path, params={'foo':True})
|
||||
r = c.get(path, params={'foo':True})
|
||||
assert r.status_code == 400
|
||||
arg = {'foo':True, 'bar':True}
|
||||
r = c.request('get', path, params=arg)
|
||||
r = c.get(path, params=arg)
|
||||
assert r.status_code == 200
|
||||
for key, val in arg.items():
|
||||
assert json.loads(r.json()[key]) == val
|
||||
assert r.json()[key] == str(val)
|
||||
|
||||
|
@ -2,5 +2,6 @@ from halfapi.halfapi import HalfAPI
|
||||
|
||||
def test_methods():
|
||||
assert 'application' in dir(HalfAPI)
|
||||
assert 'routes' in dir(HalfAPI)
|
||||
assert 'version' in dir(HalfAPI)
|
||||
assert 'version_async' in dir(HalfAPI)
|
||||
|
@ -38,7 +38,7 @@ def test_jwt_NoToken(dummy_app):
|
||||
|
||||
dummy_app.add_route('/test', test_route)
|
||||
test_client = TestClient(dummy_app)
|
||||
resp = test_client.request('get', '/test')
|
||||
resp = test_client.get('/test')
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_jwt_Token(dummy_app, token_builder):
|
||||
@ -50,20 +50,13 @@ def test_jwt_Token(dummy_app, token_builder):
|
||||
dummy_app.add_route('/test', test_route)
|
||||
test_client = TestClient(dummy_app)
|
||||
|
||||
resp = test_client.request('get', '/test',
|
||||
cookies={
|
||||
'Authorization': token_builder
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
|
||||
resp = test_client.request('get', '/test',
|
||||
resp = test_client.get('/test',
|
||||
headers={
|
||||
'Authorization': token_builder
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
|
||||
def test_jwt_DebugFalse(dummy_app, token_debug_false_builder):
|
||||
async def test_route(request):
|
||||
assert isinstance(request.user, JWTUser)
|
||||
@ -72,13 +65,7 @@ def test_jwt_DebugFalse(dummy_app, token_debug_false_builder):
|
||||
dummy_app.add_route('/test', test_route)
|
||||
test_client = TestClient(dummy_app)
|
||||
|
||||
resp = test_client.request('get', '/test',
|
||||
cookies={
|
||||
'Authorization': token_debug_false_builder
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
|
||||
resp = test_client.request('get', '/test',
|
||||
resp = test_client.get('/test',
|
||||
headers={
|
||||
'Authorization': token_debug_false_builder
|
||||
})
|
||||
@ -95,13 +82,7 @@ def test_jwt_DebugTrue(dummy_app, token_debug_true_builder):
|
||||
dummy_app.add_route('/test', test_route)
|
||||
test_client = TestClient(dummy_app)
|
||||
|
||||
resp = test_client.request('get', '/test',
|
||||
cookies={
|
||||
'Authorization': token_debug_true_builder
|
||||
})
|
||||
assert resp.status_code == 400
|
||||
|
||||
resp = test_client.request('get', '/test',
|
||||
resp = test_client.get('/test',
|
||||
headers={
|
||||
'Authorization': token_debug_true_builder
|
||||
})
|
||||
@ -119,13 +100,7 @@ def test_jwt_DebugTrue_DebugApp(dummy_debug_app, token_debug_true_builder):
|
||||
dummy_debug_app.add_route('/test', test_route)
|
||||
test_client = TestClient(dummy_debug_app)
|
||||
|
||||
resp = test_client.request('get', '/test',
|
||||
cookies={
|
||||
'Authorization': token_debug_true_builder
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
|
||||
resp = test_client.request('get', '/test',
|
||||
resp = test_client.get('/test',
|
||||
headers={
|
||||
'Authorization': token_debug_true_builder
|
||||
})
|
||||
|
@ -1,124 +1,51 @@
|
||||
# #!/usr/bin/env python3
|
||||
# import importlib
|
||||
# from halfapi.lib.domain import VERBS, gen_routes, gen_router_routes, \
|
||||
# MissingAclError, domain_schema_dict, domain_schema_list
|
||||
#
|
||||
# from types import FunctionType
|
||||
#
|
||||
#
|
||||
# def test_gen_router_routes():
|
||||
# from .dummy_domain import routers
|
||||
# for path, verb, m_router, fct, params in gen_router_routes(routers, ['dummy_domain']):
|
||||
# assert isinstance(path, str)
|
||||
# assert verb in VERBS
|
||||
# assert len(params) > 0
|
||||
# assert hasattr(fct, '__call__')
|
||||
# assert len(m_router.__file__) > 0
|
||||
#
|
||||
#
|
||||
# def test_gen_routes():
|
||||
# from .dummy_domain.routers.abc.alphabet import TEST_uuid
|
||||
# try:
|
||||
# gen_routes(
|
||||
# TEST_uuid,
|
||||
# 'get',
|
||||
# ['abc', 'alphabet', 'TEST_uuid', ''],
|
||||
# [])
|
||||
# except MissingAclError:
|
||||
# assert True
|
||||
#
|
||||
# fct, params = gen_routes(
|
||||
# TEST_uuid,
|
||||
# 'get',
|
||||
# ['abc', 'alphabet', 'TEST_uuid', ''],
|
||||
# TEST_uuid.ACLS['GET'])
|
||||
#
|
||||
# assert isinstance(fct, FunctionType)
|
||||
# assert isinstance(params, list)
|
||||
# assert len(TEST_uuid.ACLS['GET']) == len(params)
|
||||
#
|
||||
# def test_domain_schema_dict():
|
||||
# from .dummy_domain import routers
|
||||
# d_res = domain_schema_dict(routers)
|
||||
#
|
||||
# assert isinstance(d_res, dict)
|
||||
#
|
||||
# def test_domain_schema_list():
|
||||
# from .dummy_domain import routers
|
||||
# res = domain_schema_list(routers)
|
||||
#
|
||||
# assert isinstance(res, list)
|
||||
# assert len(res) > 0
|
||||
#!/usr/bin/env python3
|
||||
import importlib
|
||||
from halfapi.lib.domain import VERBS, gen_routes, gen_router_routes, \
|
||||
MissingAclError, domain_schema_dict, domain_schema_list
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
from starlette.responses import Response
|
||||
from starlette.routing import Router, Route
|
||||
from types import FunctionType
|
||||
|
||||
from halfapi.lib.domain import route_decorator
|
||||
from halfapi.lib.user import Nobody
|
||||
|
||||
def test_route_decorator():
|
||||
""" It should decorate an async function that fullfills its arguments
|
||||
"""
|
||||
def route(halfapi, data, out, ret_type='txt'):
|
||||
for key in ['user', 'config', 'domain', 'cookies', 'base_url', 'url']:
|
||||
assert key in halfapi
|
||||
def test_gen_router_routes():
|
||||
from .dummy_domain import routers
|
||||
for path, verb, m_router, fct, params in gen_router_routes(routers, ['dummy_domain']):
|
||||
assert isinstance(path, str)
|
||||
assert verb in VERBS
|
||||
assert len(params) > 0
|
||||
assert hasattr(fct, '__call__')
|
||||
assert len(m_router.__file__) > 0
|
||||
|
||||
assert halfapi['user'] is None
|
||||
assert isinstance(halfapi['config'], dict)
|
||||
assert len(halfapi['config']) == 0
|
||||
assert isinstance(halfapi['domain'], str)
|
||||
assert halfapi['domain'] == 'unknown'
|
||||
assert isinstance(halfapi['cookies'], dict)
|
||||
assert len(halfapi['cookies']) == 0
|
||||
assert len(str(halfapi['base_url'])) > 0
|
||||
assert str(halfapi['base_url']) == 'http://testserver/'
|
||||
assert len(str(halfapi['url'])) > 0
|
||||
assert str(halfapi['url']) == 'http://testserver/'
|
||||
assert isinstance(data, dict)
|
||||
assert len(data) == 0
|
||||
|
||||
assert out is None
|
||||
def test_gen_routes():
|
||||
from .dummy_domain.routers.abc.alphabet import TEST_uuid
|
||||
try:
|
||||
gen_routes(
|
||||
TEST_uuid,
|
||||
'get',
|
||||
['abc', 'alphabet', 'TEST_uuid', ''],
|
||||
[])
|
||||
except MissingAclError:
|
||||
assert True
|
||||
|
||||
assert ret_type is 'txt'
|
||||
fct, params = gen_routes(
|
||||
TEST_uuid,
|
||||
'get',
|
||||
['abc', 'alphabet', 'TEST_uuid', ''],
|
||||
TEST_uuid.ACLS['GET'])
|
||||
|
||||
return ''
|
||||
assert isinstance(fct, FunctionType)
|
||||
assert isinstance(params, list)
|
||||
assert len(TEST_uuid.ACLS['GET']) == len(params)
|
||||
|
||||
async_route = route_decorator(route)
|
||||
app = Router([Route('/', endpoint=async_route, methods=['GET'])])
|
||||
client = TestClient(app)
|
||||
response = client.get('/')
|
||||
assert response.is_success
|
||||
assert response.content.decode() == ''
|
||||
def test_domain_schema_dict():
|
||||
from .dummy_domain import routers
|
||||
d_res = domain_schema_dict(routers)
|
||||
|
||||
def route(data, out, ret_type='txt'):
|
||||
assert isinstance(data, dict)
|
||||
assert len(data) == 0
|
||||
assert isinstance(d_res, dict)
|
||||
|
||||
assert out is None
|
||||
def test_domain_schema_list():
|
||||
from .dummy_domain import routers
|
||||
res = domain_schema_list(routers)
|
||||
|
||||
assert ret_type is 'txt'
|
||||
|
||||
return ''
|
||||
|
||||
async_route = route_decorator(route)
|
||||
app = Router([Route('/', endpoint=async_route, methods=['GET'])])
|
||||
client = TestClient(app)
|
||||
response = client.get('/')
|
||||
assert response.is_success
|
||||
assert response.content.decode() == ''
|
||||
|
||||
def route(data):
|
||||
assert isinstance(data, dict)
|
||||
assert len(data) == 2
|
||||
assert data['toto'] == 'tata'
|
||||
assert data['bouboul'] == True
|
||||
|
||||
return ''
|
||||
|
||||
async_route = route_decorator(route)
|
||||
app = Router([Route('/', endpoint=async_route, methods=['POST'])])
|
||||
client = TestClient(app)
|
||||
response = client.post('/', json={'toto': 'tata', 'bouboul': True})
|
||||
assert response.is_success
|
||||
assert response.json() == ''
|
||||
assert isinstance(res, list)
|
||||
assert len(res) > 0
|
||||
|
@ -1,38 +0,0 @@
|
||||
from starlette.testclient import TestClient
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from unittest.mock import patch
|
||||
from halfapi.lib.domain_middleware import DomainMiddleware
|
||||
|
||||
def test_init():
|
||||
with patch('starlette.middleware.base.BaseHTTPMiddleware.__init__') as init:
|
||||
mw = DomainMiddleware('app', 'domain')
|
||||
init.assert_called_once_with('app')
|
||||
assert mw.domain == 'domain'
|
||||
assert mw.request == None
|
||||
|
||||
def test_call(application_debug):
|
||||
c = TestClient(application_debug)
|
||||
r = c.request('get', '/abc/alphabet')
|
||||
assert r.status_code == 200
|
||||
assert r.headers['x-domain'] == 'dummy_domain'
|
||||
assert r.headers['x-acl'] == 'public'
|
||||
|
||||
r = c.request('get', '/arguments')
|
||||
assert r.status_code == 400
|
||||
assert r.headers['x-domain'] == 'dummy_domain'
|
||||
assert r.headers['x-acl'] == 'public'
|
||||
assert 'foo' in r.headers['x-args-required'].split(',')
|
||||
assert 'bar' in r.headers['x-args-required'].split(',')
|
||||
assert r.headers['x-args-optional'] == 'x'
|
||||
|
||||
c = TestClient(application_debug)
|
||||
r = c.request('post', '/arguments')
|
||||
assert r.status_code == 400
|
||||
assert r.headers['x-domain'] == 'dummy_domain'
|
||||
assert r.headers['x-acl'] == 'public'
|
||||
assert 'foo' in r.headers['x-args-required'].split(',')
|
||||
assert 'baz' in r.headers['x-args-required'].split(',')
|
||||
assert 'truebidoo' in r.headers['x-args-optional'].split(',')
|
||||
assert 'z' in r.headers['x-args-optional'].split(',')
|
||||
|
||||
|
@ -1,57 +1,44 @@
|
||||
# import os
|
||||
# import pytest
|
||||
# from schema import SchemaError
|
||||
# from halfapi.lib.router import read_router
|
||||
# from halfapi.lib.constants import ROUTER_SCHEMA, ROUTER_ACLS_SCHEMA
|
||||
#
|
||||
# def test_read_router_routers():
|
||||
# from .dummy_domain import routers
|
||||
#
|
||||
# router_d = read_router(routers)
|
||||
# assert '' in router_d
|
||||
# assert 'SUBROUTES' in router_d['']
|
||||
# assert isinstance(router_d['']['SUBROUTES'], list)
|
||||
#
|
||||
# for elt in os.scandir(routers.__path__[0]):
|
||||
# if elt.is_dir():
|
||||
# assert elt.name in router_d['']['SUBROUTES']
|
||||
#
|
||||
# def test_read_router_abc():
|
||||
# from .dummy_domain.routers import abc
|
||||
# router_d = read_router(abc)
|
||||
#
|
||||
# assert '' in router_d
|
||||
# assert 'SUBROUTES' in router_d['']
|
||||
# assert isinstance(router_d['']['SUBROUTES'], list)
|
||||
#
|
||||
# def test_read_router_alphabet():
|
||||
# from .dummy_domain.routers.abc import alphabet
|
||||
# router_d = read_router(alphabet)
|
||||
#
|
||||
# assert '' in router_d
|
||||
# assert 'SUBROUTES' in router_d['']
|
||||
# assert isinstance(router_d['']['SUBROUTES'], list)
|
||||
#
|
||||
# ROUTER_SCHEMA.validate(router_d)
|
||||
#
|
||||
# with pytest.raises(SchemaError):
|
||||
# """ Test that we cannot specify wrong method in ROUTES or ACLS
|
||||
#
|
||||
# TODO: Write more errors
|
||||
# """
|
||||
# router_d['']['TEG'] = {}
|
||||
# ROUTER_SCHEMA.validate(router_d)
|
||||
#
|
||||
# def test_read_router_TEST():
|
||||
# from .dummy_domain.routers.abc.alphabet import TEST_uuid
|
||||
# router_d = read_router(TEST_uuid)
|
||||
#
|
||||
# print(router_d)
|
||||
# assert '' in router_d
|
||||
# assert 'SUBROUTES' in router_d['']
|
||||
# assert isinstance(router_d['']['GET'], list)
|
||||
# assert isinstance(router_d['']['POST'], list)
|
||||
# assert isinstance(router_d['']['PATCH'], list)
|
||||
# assert isinstance(router_d['']['PUT'], list)
|
||||
#
|
||||
#
|
||||
import os
|
||||
from halfapi.lib.router import read_router
|
||||
|
||||
def test_read_router_routers():
|
||||
from .dummy_domain import routers
|
||||
|
||||
router_d = read_router(routers)
|
||||
assert '' in router_d
|
||||
assert 'SUBROUTES' in router_d['']
|
||||
assert isinstance(router_d['']['SUBROUTES'], list)
|
||||
|
||||
for elt in os.scandir(routers.__path__[0]):
|
||||
if elt.is_dir():
|
||||
assert elt.name in router_d['']['SUBROUTES']
|
||||
|
||||
def test_read_router_abc():
|
||||
from .dummy_domain.routers import abc
|
||||
router_d = read_router(abc)
|
||||
|
||||
assert '' in router_d
|
||||
assert 'SUBROUTES' in router_d['']
|
||||
assert isinstance(router_d['']['SUBROUTES'], list)
|
||||
|
||||
def test_read_router_alphabet():
|
||||
from .dummy_domain.routers.abc import alphabet
|
||||
router_d = read_router(alphabet)
|
||||
|
||||
assert '' in router_d
|
||||
assert 'SUBROUTES' in router_d['']
|
||||
assert isinstance(router_d['']['SUBROUTES'], list)
|
||||
|
||||
def test_read_router_TEST():
|
||||
from .dummy_domain.routers.abc.alphabet import TEST_uuid
|
||||
router_d = read_router(TEST_uuid)
|
||||
|
||||
print(router_d)
|
||||
assert '' in router_d
|
||||
assert 'SUBROUTES' in router_d['']
|
||||
assert isinstance(router_d['']['GET'], list)
|
||||
assert isinstance(router_d['']['POST'], list)
|
||||
assert isinstance(router_d['']['PATCH'], list)
|
||||
assert isinstance(router_d['']['PUT'], list)
|
||||
|
||||
|
||||
|
@ -1,34 +1,34 @@
|
||||
# from starlette.routing import Route
|
||||
# from halfapi.lib.routes import gen_starlette_routes, gen_router_routes
|
||||
#
|
||||
# def test_gen_starlette_routes():
|
||||
# from .dummy_domain import routers
|
||||
# for route in gen_starlette_routes({
|
||||
# 'dummy_domain': routers }):
|
||||
#
|
||||
# assert isinstance(route, Route)
|
||||
#
|
||||
# import pytest
|
||||
#
|
||||
# @pytest.mark.skip
|
||||
# def test_api_routes():
|
||||
# from . import dummy_domain
|
||||
# d_res, d_acls = api_routes(dummy_domain)
|
||||
# assert isinstance(d_res, dict)
|
||||
# assert isinstance(d_acls, dict)
|
||||
#
|
||||
# yielded = False
|
||||
#
|
||||
# for path, verb, m_router, fct, params in gen_router_routes(dummy_domain, []):
|
||||
# if not yielded:
|
||||
# yielded = True
|
||||
#
|
||||
# assert path in d_res
|
||||
# assert verb in d_res[path]
|
||||
# assert 'docs' in d_res[path][verb]
|
||||
# assert 'acls' in d_res[path][verb]
|
||||
# assert isinstance(d_res[path][verb]['docs'], dict)
|
||||
# assert isinstance(d_res[path][verb]['acls'], list)
|
||||
# assert len(d_res[path][verb]['acls']) == len(params)
|
||||
#
|
||||
# assert yielded is True
|
||||
from starlette.routing import Route
|
||||
from halfapi.lib.routes import gen_starlette_routes, gen_router_routes
|
||||
|
||||
def test_gen_starlette_routes():
|
||||
from .dummy_domain import routers
|
||||
for route in gen_starlette_routes({
|
||||
'dummy_domain': routers }):
|
||||
|
||||
assert isinstance(route, Route)
|
||||
|
||||
import pytest
|
||||
|
||||
@pytest.mark.skip
|
||||
def test_api_routes():
|
||||
from . import dummy_domain
|
||||
d_res, d_acls = api_routes(dummy_domain)
|
||||
assert isinstance(d_res, dict)
|
||||
assert isinstance(d_acls, dict)
|
||||
|
||||
yielded = False
|
||||
|
||||
for path, verb, m_router, fct, params in gen_router_routes(dummy_domain, []):
|
||||
if not yielded:
|
||||
yielded = True
|
||||
|
||||
assert path in d_res
|
||||
assert verb in d_res[path]
|
||||
assert 'docs' in d_res[path][verb]
|
||||
assert 'acls' in d_res[path][verb]
|
||||
assert isinstance(d_res[path][verb]['docs'], dict)
|
||||
assert isinstance(d_res[path][verb]['acls'], list)
|
||||
assert len(d_res[path][verb]['acls']) == len(params)
|
||||
|
||||
assert yielded is True
|
||||
|
30
tests/test_lib_schemas.py
Normal file
30
tests/test_lib_schemas.py
Normal file
@ -0,0 +1,30 @@
|
||||
import subprocess
|
||||
from pprint import pprint
|
||||
from starlette.testclient import TestClient
|
||||
from starlette.authentication import (
|
||||
AuthenticationBackend, AuthenticationError, BaseUser, AuthCredentials,
|
||||
UnauthenticatedUser)
|
||||
|
||||
from halfapi.lib.schemas import schema_dict_dom, schema_to_csv, schema_csv_dict
|
||||
from halfapi.lib.constants import DOMAIN_SCHEMA, API_SCHEMA
|
||||
|
||||
from halfapi import __version__
|
||||
|
||||
def test_schemas_dict_dom():
|
||||
from .dummy_domain import routers
|
||||
schema = schema_dict_dom({
|
||||
'dummy_domain':routers})
|
||||
assert isinstance(schema, dict)
|
||||
|
||||
def test_schema_to_csv():
|
||||
csv = schema_to_csv('dummy_domain.routers', False)
|
||||
assert isinstance(csv, str)
|
||||
assert len(csv.split('\n')) > 0
|
||||
|
||||
def test_schema_csv_dict():
|
||||
csv = schema_to_csv('dummy_domain.routers', False)
|
||||
assert isinstance(csv, str)
|
||||
schema_d = schema_csv_dict(csv.split('\n'))
|
||||
assert isinstance(schema_d, dict)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user