Compare commits

..

445 Commits

Author SHA1 Message Date
41ba577afa
[release] halfapi 0.6.31 2025-05-17 01:32:10 +02:00
f9bef2b124
[deps] bump versions for pyjwt and schema 2025-05-17 01:27:04 +02:00
ee3bd4413c
[deps] starlette v0.46.2 2025-05-17 01:13:55 +02:00
50a8431ca3
[deps] bump versions in Pipfile.lock, remove mix_stderr with "click" new version, needs orjson>=3.10 2025-05-17 00:59:27 +02:00
c808ba21ab
[fix] oubli import 2024-05-28 03:40:29 +02:00
9f908d3cee
[release] halfapi 0.6.30 2024-05-28 02:55:17 +02:00
7b6d9e994a
[deps] starlette v0.37 2024-05-28 02:54:50 +02:00
8f6330bca7
[deps] starlette 0.35 2024-05-28 02:53:58 +02:00
8506aa5322
[middleware] fix DomainMiddleware constructor for 0.35 2024-05-28 02:41:41 +02:00
b683e80959
[halfapi] remove on_startup argument according to starlette 0.26 2024-05-28 02:41:19 +02:00
b412d249a1
[deps] bump dependencies versions 2024-05-28 02:39:08 +02:00
Maxime Alves LIRMM
f4f9a0fc66 [dockerfile] alpine 3.19 - utilisation d'un venv 2023-12-13 11:37:29 +01:00
Maxime Alves LIRMM
c855cce013 [release] 0.6.29 2023-12-13 11:09:42 +01:00
Maxime Alves LIRMM
e083c4386e [release] 0.6.28 2023-11-24 14:31:10 +01:00
Maxime Alves LIRMM
476ae29792 [rc] 0.6.28rc5 2023-09-01 04:59:17 +09:00
Maxime Alves LIRMM
673097adeb [half_domain] fix ACL route when checked ACL is a decorator 2023-09-01 04:58:12 +09:00
Maxime Alves LIRMM
1cc1bbd5ef [rc] 0.6.28rc4 (remove rc3) 2023-08-21 08:47:47 +02:00
Maxime Alves LIRMM
135d6e86e4 [conf] fix export SECRET variable 2023-08-21 08:47:19 +02:00
Maxime Alves LIRMM
0fcf433ec6 [ci] use alpine base image directly 2023-08-21 00:08:29 +02:00
Maxime Alves LIRMM
45cf32de2b [ci] utilisation de l'image 3.17 (pour fix le bug exceptiongroup) 2023-08-20 23:53:18 +02:00
Maxime Alves LIRMM
1b713c3816 [ci] fix image pour les tests <3.11 2023-08-20 23:42:10 +02:00
Maxime Alves LIRMM
59889e1e31 [ci] pipenv --skip-lock (has been remove) 2023-08-20 23:34:34 +02:00
Maxime Alves LIRMM
28a1a69435 [rc] 0.6.28rc3 - fix bugs and general configuration management cleanup (see changelog) 2023-08-20 23:32:50 +02:00
65ecf9817c [ci/Dockerfile] use of alpine OS instead of debian, bump python to version 3.11 2023-08-09 14:46:52 +02:00
3b7d3bda5c [rc] 0.6.28rc2 2023-08-09 14:24:32 +02:00
e19f27f306 [deps] starlette 0.31 2023-08-09 14:23:09 +02:00
e9c84c9f7c [rc] 0.6.28-rc1 2023-08-08 09:09:16 +02:00
b1595beb14 [tests] write a schema component for dummy_domain for example 2023-08-02 13:29:57 +02:00
60ff99d0fb [domain] you can specify Schema components under the "schema_components" key of the domain dictionary 2023-08-02 12:55:04 +02:00
9657f0f9ec [ci-cd] add python 3.11 to testing releases 2023-08-02 10:57:33 +02:00
f646b4d663 [ci-cd] coverage report 2023-08-02 10:49:54 +02:00
0817882558 [dev-deps] ajout de la dépendance a coverage 2023-08-02 10:43:50 +02:00
896ce58731 [pytest] do not avoid to run doctest modules in halfapi sources! 2023-08-02 10:39:06 +02:00
87856cfb42 [cli-conf] halfapi domain : the file provided as argument is a toml file of the format of .halfapi/config, + better config handling 2023-08-02 10:38:36 +02:00
4856f80b99 [rc] 0.6.28-rc0-1 2023-08-02 06:32:26 +02:00
eac602f0a5 [rc] 0.6.28rc0 2023-08-01 20:50:58 +02:00
14e051bd91 [doc-schema] the "/" route on a domain now returns the OpenAPI-validated Schema (not a list of schemas), the "dummy_domain" test now validates OpenAPI specs 2023-08-01 20:31:17 +02:00
20563081f5 [doc-schema] In module-based routers, if there is a path parameter, you can specify an OpenAPI documentation for it, or a default will be used 2023-08-01 20:24:24 +02:00
7949b3206c [dev-deps] openapi-schema-validator, openapi-spec-validator 2023-08-01 19:41:49 +02:00
c4583b7187 [doc] add docstrings for halfapi routes 2023-08-01 17:43:59 +02:00
2413436104 [acl] The public acls check routes use the "HEAD" method, deprecated "GET" 2023-08-01 17:32:25 +02:00
Maxime Alves LIRMM
54cc6c17c9 [release] 0.6.27 2023-06-07 11:46:53 +02:00
Maxime Alves LIRMM
ff3a39c740 [rc] 0.6.27rc0 2023-06-01 15:39:57 +02:00
Maxime Alves LIRMM@home
8d254bafa0 [feature] changes in the ACLs result availability 2023-06-01 15:39:44 +02:00
Maxime Alves LIRMM
0a385661b9 [rc] 0.6.26rc0 2023-02-23 11:44:57 +01:00
Maxime Alves LIRMM
e065fe04e4 [tests] test with multiple optional parameteres 2023-02-21 19:30:59 +01:00
Maxime Alves LIRMM
b7c5704c95 [tests] fix tests so the data is sent in json 2023-02-21 19:23:23 +01:00
Maxime Alves LIRMM
dd83a337e9 [lib.domain] route_decorator : Adds the "base_url", "cookies" and "url" to the "halfapi" argument of route definitions 2023-02-21 19:22:57 +01:00
Maxime Alves LIRMM
f6d08e8309 [release] 0.6.25 2023-02-20 12:18:38 +01:00
Maxime Alves LIRMM
262de901a8 [jwtMiddleware] on expired signature error, returns Nobody and do not raise an exception 2023-02-13 11:14:24 +01:00
Maxime Alves LIRMM
e5c25ede1f [rc] 0.6.25-rc0 2023-02-08 12:54:35 +01:00
Maxime Alves LIRMM
b4c37ea999 [release] 0.6.24 2023-02-03 14:22:04 +01:00
Maxime Alves LIRMM
5a7e51ae94 [jwtMiddleware] clean "is_fake_user_id" code 2023-02-03 14:20:01 +01:00
Maxime Alves LIRMM
69129fd7af [doc][release] 0.7.0 2023-02-03 12:43:30 +01:00
Maxime Alves LIRMM
a3fc6dc830 [authMiddleware] UN-Breaking uses either the cookie or the header names "Authorization" 2023-02-03 12:43:16 +01:00
Maxime Alves LIRMM@home
064127dc16 [fix] tests for running by implementing the dry-run option to the domain command 2023-02-03 00:02:56 +01:00
Maxime Alves LIRMM@home
c2eb95533c [fix] fix check_domain test c9ecc1f8d2 2023-02-02 22:33:35 +01:00
Maxime Alves LIRMM
bbb027cd0d [authMiddleware][BREAKING] read token from "JWTToken" cookie 2023-02-02 19:56:59 +01:00
Maxime Alves LIRMM
c9ecc1f8d2 [cli] allow to run halfapi on a specified domain using the "halfapi domain --run DOMAIN" command 2023-02-02 19:56:11 +01:00
Maxime Alves LIRMM
d1a8351997 [halfapi] if there is only one domain it is automatically enabled 2023-02-02 19:55:37 +01:00
Maxime Alves LIRMM
dc29abea84 [release] 0.6.23 2023-01-14 12:03:40 +01:00
Maxime Alves LIRMM
4c966e7151 [deps] toml bumps version 0.10 2023-01-14 11:43:38 +01:00
Maxime Alves LIRMM
7deb353b4b [deps] PyYAML bumps version 6.0 2023-01-14 11:39:40 +01:00
Maxime Alves LIRMM
3986083725 [deps] PyJWT bumps version 2.6.0 2023-01-14 11:37:08 +01:00
Maxime Alves LIRMM
301b0eeab5 [deps] click bump version 8 2023-01-14 11:29:14 +01:00
Maxime Alves LIRMM
cc0566542b [deps] orjson bump version 3.8.5 2023-01-14 11:23:25 +01:00
Maxime Alves LIRMM
4f4dac0ff2 [deps] Migration from starlette v0.18 to v0.23
Breaking : migrate your tests that use the TestDomain.client method
following the instructions here https://github.com/Kludex/bump-testclient

Squashed commit of the following:

commit 0417f27b3f
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Sat Jan 14 11:08:44 2023 +0100

    [deps] starlette 0.23

commit 552f00a65b
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Sat Jan 14 10:59:42 2023 +0100

    [deps] starlette 0.22

commit aefe448717
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Sat Jan 14 10:55:45 2023 +0100

    [tests][fix] compares the json interpreted value instead of the string

commit 01333a200c
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Sat Jan 14 10:55:20 2023 +0100

    [testing] changes from requests to httpx for Starlette TestClient (breaks)

commit f3784fab7f
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Sat Jan 14 10:54:10 2023 +0100

    [deps][breaking] starlette 0.21

commit 717d3f8bd6
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Sat Jan 14 10:26:31 2023 +0100

    [responses] use a wrapper function for exception handling (fix starlette 0.20)

commit d0876e45da
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Sat Jan 14 10:25:21 2023 +0100

    [deps][breaking] starlette 0.20

commit 6504191c53
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Sat Jan 14 10:12:51 2023 +0100

    [deps] starlette 0.19

commit 7b639a8dc2
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Sat Jan 14 10:11:14 2023 +0100

    [deps] starlette 0.18

commit 20bd9077a4
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Sat Jan 14 10:07:48 2023 +0100

    pipenv update
2023-01-14 11:10:13 +01:00
Maxime Alves LIRMM
8b8caa2e15 [release] 0.6.22 2022-12-02 18:11:26 +01:00
Maxime Alves LIRMM
5a70c00541 [rc] 0.6.22-rc6 2022-10-13 12:21:26 +02:00
Maxime Alves LIRMM
7723acb812 [breaking] domain description is defined in a dict, not in "__name__", "__id__", "__routers__" ... 2022-10-13 12:18:13 +02:00
Maxime Alves LIRMM
fd682ba0e0 [fix] wrong "data" argument value when there is a default one 2022-09-13 19:15:45 +02:00
Maxime Alves LIRMM@home
e9c57049dd [lib/domain] log.error unhandled exceptions 2022-09-06 21:51:48 +02:00
Maxime Alves LIRMM
e47ffcddb9 [deps] update pyJWT>2.4.0 2022-09-06 19:58:41 +02:00
Maxime Alves LIRMM
f0c898ba20 (0.6.22rc3) [halfapi] Log HTTPException with statuscode 500 as critical 2022-09-05 10:44:04 +02:00
Maxime Alves LIRMM@home
b7e678e00f [rc] 0.6.22-rc2 2022-09-05 10:20:08 +02:00
Maxime Alves LIRMM@home
d0ec030ce9 [fix] important fix for argument handling 2022-09-05 10:18:57 +02:00
Maxime Alves LIRMM@home
039bc2c8fe [tests][BREAK] arguments are not filtered (since version 0.6.20 probably) 2022-09-05 10:12:47 +02:00
Maxime Alves LIRMM@home
5e21d4c24f fix ret_type feature 2022-08-31 00:14:10 +02:00
Maxime Alves LIRMM@home
8bdf5cab82 [testing] MODULE and CONFIG attributes 2022-08-31 00:13:29 +02:00
Maxime Alves LIRMM
910e1e1497 [release] 0.6.22-rc0 2022-08-18 20:24:57 +02:00
Maxime Alves LIRMM
53a691d985 [responses] html and plaintext return types as ret_type argument 2022-08-18 20:22:14 +02:00
Maxime Alves LIRMM@home
5d1b88daca [release] 0.6.21 2022-08-08 20:26:31 +02:00
Maxime Alves LIRMM@home
c0bd6ddc43 [release] 0.6.21 2022-08-08 20:21:05 +02:00
Maxime Alves LIRMM
779dd2d519 [deps] lock pipenv update 2022-07-18 20:35:49 +02:00
Maxime Alves LIRMM
84747e3f73 [release] 0.6.20-rc0 2022-07-18 20:34:12 +02:00
Maxime Alves LIRMM
18748808c9 [fix] argument handling 2022-07-18 18:47:38 +02:00
Maxime Alves LIRMM
34ede09fe2 [release] 0.6.19 2022-07-06 08:34:11 +02:00
Maxime Alves LIRMM
6ebdf765bd [deps] pytest v7, [ci/cd] no tests on python3.6 2022-07-06 08:29:29 +02:00
Maxime Alves LIRMM
2d47789f61 [ci/cd] parallel tests on multiple python versions 2022-07-06 08:15:49 +02:00
3ec6d7514e [release] 0.6.18 2022-06-23 15:48:58 +02:00
3c97d39cdc [config] restore dumped configuration when using HALFAPI_DOMAIN_NAME 2022-06-23 15:33:19 +02:00
f96c712aa8 [release] 0.6.17 2022-06-23 11:07:41 +02:00
23a93026aa [halfdomain] fix feature HALFAPI_DOMAIN_MODULE for acls route 2022-06-23 11:03:52 +02:00
05cf37c775 [deps] ajout virtualenv dans les deps (pour build) 2022-06-23 11:00:33 +02:00
a46b045ca6 [release] 0.6.16 2022-06-23 07:24:05 +02:00
f68b7e59b8 handle HALFAPI_DOMAIN_MODULE environment variable 2022-06-23 07:21:49 +02:00
Maxime Alves LIRMM@home
b17ce623f4 [release] 0.6.15 2022-05-17 16:19:41 +02:00
Maxime Alves LIRMM@home
7dd7d00625 [halfdomain] fix last modification 2022-05-17 16:13:33 +02:00
Maxime Alves LIRMM@home
389823db82 [deps] Pipfile lock 2022-05-17 16:02:23 +02:00
Maxime Alves LIRMM@home
e70239433f [halfdomain] add ability to specify a domain's acl module path in __acl__ attribute 2022-05-17 16:01:58 +02:00
Maxime Alves LIRMM@home
739ffc9afa [release] 0.6.14 2022-05-17 10:31:34 +02:00
Maxime Alves LIRMM@home
99d4aaeb8d [lib/responses] ajout format XLSX, car ODS bug avec les dates
https://github.com/pyexcel/pyexcel-ods/issues/31
2022-05-17 10:30:15 +02:00
Maxime Alves LIRMM
0d9dc2a018 [release] 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)
2022-03-21 09:51:04 +01:00
Maxime Alves LIRMM
b63b0f52c6 [domain] checks versions specified in __deps__ variable of domain module 2022-03-21 09:45:35 +01:00
Maxime Alves LIRMM
63d6d1e8ea [release] 0.6.12 2022-03-17 17:48:41 +01:00
Maxime Alves LIRMM
e8c99e6012 [ci/cd] install pytest in docker image, run pytest at launch 2022-03-17 17:38:20 +01:00
Maxime Alves LIRMM
568aea9ea8 [clena] nettoyage des fonctions non utilisées 2022-03-16 13:09:50 +01:00
Maxime Alves LIRMM
988a1e5bab [ci/cd] fix kaniko call 2022-03-14 15:24:57 +01:00
Maxime Alves LIRMM
de72e469d2 [release] 0.6.11 2022-03-14 15:16:06 +01:00
Maxime Alves LIRMM
f7879c6388 [release] 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

- Hide data fields in args_check logs

- Fix testing lib for domains (add default secret and debug option)

- Domains now need to include the following variables in their __init__.py
    - __name__ (str, optional)
    - __id__ (str, optional)
- halfapi domain

- Mounts domain routers with their ACLs as decorator
- Configuration example files for systemd and a system-wide halfapi install
- Runs projects
- Handles JWT authentication middleware
2022-03-08 19:24:24 +01:00
Maxime Alves LIRMM
84179743a6 [release] 0.6.9 2022-03-02 16:46:32 +01:00
Maxime Alves LIRMM
adf7f872b6 changelog 2022-02-28 10:21:01 +01:00
Maxime Alves LIRMM
b96f4908c6 [release] 0.6.8 2022-02-28 10:10:06 +01:00
Maxime Alves LIRMM
a388faf1d8 [testing] add "secret" and "production" variable to test_domain configuration 2022-02-28 09:58:47 +01:00
Maxime Alves LIRMM
4d6e935813 [ci/cd] fix kaniko executor 2022-02-10 16:31:26 +01:00
Maxime Alves LIRMM
4e080f805f [fix] odsresponse 2022-02-10 16:30:08 +01:00
Maxime Alves LIRMM
91dd4cbaa8 [ci/cd] check tag and not branch master 2022-02-10 16:01:38 +01:00
Maxime Alves LIRMM
209c6ef40a [ci/cd] add rules to execute build stage 2022-02-10 14:31:30 +00:00
Maxime Alves LIRMM
a1c1bf04df [ci/cd] stages 2022-02-10 15:15:08 +01:00
Maxime Alves LIRMM
90203b2edf [ci] add ./tests to pytest command 2022-02-10 12:38:54 +01:00
Maxime Alves LIRMM
a69d2b7639 [tests] async router renamed to async_router 2022-02-10 12:30:49 +01:00
Maxime Alves LIRMM
c3153921f7 [tests] test_domain import dummy_domain inside test class 2022-02-10 11:59:40 +01:00
Maxime Alves LIRMM
97fee8ca96 [ci] execute pytest on tests folder 2022-02-10 11:56:35 +01:00
Maxime Alves LIRMM
d33c82e348 [0.6.7] fix ODSResponse 2022-02-10 10:30:19 +01:00
Maxime Alves LIRMM@home
058121d985 [release] 0.6.6 2022-01-21 14:39:46 +01:00
Maxime Alves LIRMM@home
2e5680d29a [auth] dont activate authenticationMiddleware if secret is missing. NO SECRET ONLY IN FULLY PUBLIC DOMAINS!!! 2022-01-21 14:39:34 +01:00
Maxime Alves LIRMM@home
979007f287 [fix][log] active domains list logging 2022-01-21 14:38:28 +01:00
Maxime Alves LIRMM@home
df555c7d26 [fix] when running one-domain mode we should set the configuration as if the domain was activated 2022-01-21 14:37:54 +01:00
Maxime Alves LIRMM@home
be312d4b7a [fix][run] wont pop keys if we dont operate on a copy 2022-01-21 14:32:55 +01:00
Maxime Alves LIRMM
f02a97fbf8 [testing] set starlette client in setUp 2021-12-15 12:37:49 +01:00
Maxime Alves LIRMM@home
e9ffb553c8 [release] 0.6.4 2021-12-14 09:05:50 +01:00
Maxime Alves LIRMM@home
776cc8c85e [testing] fix check_routes of test_domain 2021-12-14 09:04:06 +01:00
Maxime Alves LIRMM@home
5d5ffdfb7c [tests] re-enable dummy_project route testing 2021-12-14 09:03:45 +01:00
Maxime Alves LIRMM@home
7c0f5717f4 [half_route] error handling 2021-12-14 09:01:38 +01:00
Maxime Alves LIRMM
7fb5e25411 [release] 0.6.3 2021-12-13 14:47:21 +01:00
Maxime Alves LIRMM
a0dbbca04d [fix] forgot __init__.py in testing 2021-12-13 14:45:00 +01:00
Maxime Alves LIRMM
95fb267e81 [release] 0.6.2 2021-12-13 13:50:52 +01:00
Maxime Alves LIRMM
e9bf94a607 [cli/domain] now instantiate HalfAPI and manually add the domain before calling schema function 2021-12-13 13:49:58 +01:00
Maxime Alves LIRMM
f82cd5552b [half_domain] remove "config" argument, now uses "config" attribute from HalfAPI instance, add version and halfapi_versrion attributes, update DomainMiddleware init arguments 2021-12-13 12:45:10 +01:00
Maxime Alves LIRMM
bdbad9e296 [halfapi] now inherits from Starlette class. Uses a dict to store HalfDomain instances 2021-12-13 12:43:26 +01:00
Maxime Alves LIRMM
76e942ab91 [tests] add test file for lib/domain_middleware 2021-12-13 12:42:12 +01:00
Maxime Alves LIRMM
8fff1f5372 [tests] fix tests and add "mix_stderr=False" to CliRunner instance 2021-12-13 12:41:33 +01:00
Maxime Alves LIRMM
048c9f1bab [tests] fix for schema lists 2021-12-08 16:45:00 +01:00
Maxime Alves LIRMM
d5f39a7929 [schemas] always give a list of schemas 2021-12-08 16:42:12 +01:00
Maxime Alves LIRMM
648841d90f [dummy_domain] set an id to dummy_domain 2021-12-08 15:34:00 +01:00
Maxime Alves LIRMM
c658815eb5 [cli] fix halfapi domain --read 2021-12-08 13:08:08 +01:00
Maxime Alves LIRMM
46e62575ae [half_domain] fix test_dummy_project 2021-12-08 12:23:47 +01:00
Maxime Alves LIRMM
7001cec86e [wip] refactor half_domain 2021-12-07 11:42:02 +01:00
Maxime Alves LIRMM@home
b4157c4a7d [wip][testfail] multiple-domains app 2021-12-07 07:53:36 +01:00
Maxime Alves LIRMM@home
96f78e76c5 [tests][testfail] add default routes testing, /halfapi/acls fail 2021-12-04 09:56:14 +01:00
Maxime Alves LIRMM@home
d54dcd641d [conf] fix secret tempfile creation 2021-12-04 00:28:30 +01:00
Maxime Alves LIRMM
7060d201ec [deps] optional requirements for ODSResponse 2021-12-03 17:26:14 +01:00
Maxime Alves LIRMM
dbca2f28fb [conf] use of toml for halfapi configs. re-enable possibility of multiple domains 2021-12-03 17:25:57 +01:00
Maxime Alves LIRMM@home
d06857bf49 [config][wip][nf] removal of "domains" and "domain" section 2021-12-03 09:49:30 +01:00
Maxime Alves LIRMM@home
3dc951c81e [tests] tidy cli tests 2021-12-03 09:20:40 +01:00
Maxime Alves LIRMM@home
a8e5cfc0ff [wip][responses] allow to change return format with "format" route argument, add ODSResponse 2021-12-01 21:16:19 +01:00
Maxime Alves LIRMM@home
20cada4fa0 [halfapi] fix domain importlib 2021-12-01 21:14:17 +01:00
Maxime Alves LIRMM@home
c1bb637be7 [lib.router] forbid extra-keys in routes dict (no more FQTN at same level of methods) 2021-12-01 21:13:35 +01:00
Maxime Alves LIRMM@home
a37c2356d6 [lib.domain] error log when missing docstring 2021-12-01 21:12:19 +01:00
Maxime Alves LIRMM@home
038715e94a [halfapi] config option "--dry-run", used in test_domain 2021-12-01 21:11:26 +01:00
Maxime Alves LIRMM@home
2f9005a1a5 [release] 0.6.1 2021-12-01 17:02:56 +01:00
Maxime Alves LIRMM@home
a2d79f49b9 [tests] dummy domain test (you can use this example in your own domain) 2021-12-01 17:02:23 +01:00
Maxime Alves LIRMM@home
cf98b08fa5 [tests] mv base domain test to halfapi/testing 2021-12-01 17:01:56 +01:00
Maxime Alves LIRMM@home
c1191bbb0e [deps] update deps 2021-12-01 16:25:53 +01:00
Maxime Alves LIRMM@home
837c646bc5 [ci] switch to bullseye-3.9 2021-12-01 15:49:59 +01:00
Maxime Alves LIRMM@home
49c13c56ac [lib.domain] implement domain schema - try: halfapi domain dummy_domain 2021-12-01 13:31:46 +01:00
Maxime Alves LIRMM@home
1ccfa0d10e [lib.schemas] router schema update 2021-12-01 13:07:01 +01:00
Maxime Alves LIRMM@home
238bd99bd3 [cli.routes] add json schema export 2021-12-01 13:06:00 +01:00
Maxime Alves LIRMM@home
429a90d786 [halfapi] fix typo 2021-12-01 12:21:26 +01:00
Maxime Alves LIRMM@home
1ec244b60f [lib.constants] route keys are optional, in case of empty routers 2021-12-01 12:20:32 +01:00
Maxime Alves LIRMM@home
53ecbb58fc [tests] rework some tests, avoid calling project_runner multiple times (should be tested better, but for now is just buggy imports...) 2021-12-01 12:20:01 +01:00
Maxime Alves LIRMM@home
7e7bbb3a62 [tests] fix de "project_runner" auto clean sys.path, "tree" fixture to debug projects 2021-12-01 12:16:31 +01:00
Maxime Alves LIRMM@home
5e88109b3e manipuler le sys.path : BAAAD 2021-12-01 12:14:27 +01:00
Maxime Alves LIRMM@home
e293ac3867 [cli/domain] create domain function rework - creates a basic domain 2021-12-01 12:08:24 +01:00
Maxime Alves LIRMM@home
a98aa27485 [pipfile] remove dependency to python3.8, TODO find a way to specify python>=3.8 2021-12-01 12:02:21 +01:00
Maxime Alves LIRMM@home
15794327f9 [tests] pytest.ini includes tests as pythonpath (because of dummy_domain) 2021-12-01 11:55:56 +01:00
Maxime Alves LIRMM@home
cf20b76959 [deps] add pytest-pythonpath 2021-12-01 11:43:49 +01:00
Maxime Alves LIRMM
7e1cc21b8c [wip][testfail] update config monodomain - schema gives acls 2021-11-30 18:31:40 +01:00
Maxime Alves LIRMM
ec26438340 [schemas] supression des fonctions api_routes 2021-11-30 14:40:07 +01:00
Maxime Alves LIRMM
7230316296 [ci] add pytest version 2021-11-30 12:13:17 +01:00
Maxime Alves LIRMM
f5b7e3392a [tests] skip /halfapi/exception test 2021-11-30 12:07:19 +01:00
Maxime Alves LIRMM
18dbbdd584 [app] enable use of SCHEMA to run halfapi, fix tests 2021-11-30 11:20:26 +01:00
Maxime Alves LIRMM
b3b32b47f8 [cli.run] halfapi run SCHEMA.csv 2021-11-30 10:42:00 +01:00
Maxime Alves LIRMM
189fcf86f7 [cli.routes] halfapi routes --export 2021-11-30 01:54:48 +01:00
Maxime Alves LIRMM
55878df260 [lib.domain] use args_check by default, even on async functions 2021-11-30 01:52:02 +01:00
Maxime Alves LIRMM
a6985fa9bf [halfapi] configuration globals go lowercase 2021-11-30 01:50:27 +01:00
Maxime Alves LIRMM
ed6dcb0513 [halfapi] application becomes a private attribute 2021-11-30 01:49:37 +01:00
Maxime Alves LIRMM
7017827b2b [lib] small fixes 2021-11-30 01:46:30 +01:00
Maxime Alves LIRMM
7c2bf60812 [pytest] remove halfapi from tests directory 2021-11-30 01:43:14 +01:00
Maxime Alves LIRMM
1fda2ab15d [lib.schemas] add schema_csv_dict and schema_to_csv function 2021-11-30 01:42:29 +01:00
Maxime Alves LIRMM
c9639ddbc0 [constants] add lib "schema" Schemas for all items 2021-11-30 00:37:13 +01:00
Maxime Alves LIRMM
24bd3f5653 [deps] add "schema" as dependency 2021-11-29 17:01:52 +01:00
Maxime Alves LIRMM@home
e203552876 [dummy_domain] add async routes, and routes that use arguments 2021-11-29 08:37:52 +01:00
Maxime Alves LIRMM@home
ac4aadc2df [conf][testfail] add SCHEMA dictionary to globals 2021-11-29 06:21:48 +01:00
Maxime Alves LIRMM@home
4dae2f3676 [dummy_domain] add tests for dummy_domain acls 2021-11-29 06:09:18 +01:00
Maxime Alves LIRMM@home
c27ed3a966 [routers] docstring is mandator in route methods. YAML is used for markup 2021-11-29 06:08:24 +01:00
Maxime Alves LIRMM@home
47d81c048f [lib] route_acl_decorator becomes HalfRoute.acl_decorator, creation of HalfRoute that wraps starlette.route 2021-11-29 05:42:26 +01:00
Maxime Alves LIRMM
ad6877a7e9 [run] add arguments for config 2021-11-23 13:53:08 +01:00
Maxime Alves LIRMM
e7e1bfed1b [conf] halfapi config now dumps the .ini of the current config 2021-11-23 13:35:12 +01:00
Maxime Alves LIRMM
f0e662e060 [conf] remove is_project variable 2021-11-23 11:39:33 +01:00
Maxime Alves LIRMM
ea8e7ede65 [cli.routes:export] shows a header to describe the CSV, can be deactivated 2021-11-22 20:14:05 +01:00
Maxime Alves LIRMM
f4880f1f9c [lib.domain] wraps function when loading routes from modules, to get the real name 2021-11-22 20:09:57 +01:00
Maxime Alves LIRMM
4a8cb008e6 [cli.routes] add routes command, allows exporting route dict as csv 2021-11-22 20:06:50 +01:00
Maxime Alves LIRMM
049860fce5 [lib.domain] gen_router_routes yields router module also 2021-11-22 20:06:12 +01:00
Maxime Alves LIRMM
908eab5fdc [lib.user] move JWTUser, Nobody and CheckUser to lib.user 2021-11-22 20:05:17 +01:00
Maxime Alves LIRMM
ce672eeb30 [cli.domain] fix list routes 2021-11-22 19:09:02 +01:00
Maxime Alves LIRMM
1f20a336e2 [conf] production now defaults to true 2021-11-22 18:30:29 +01:00
Maxime Alves LIRMM
0173eb6d72 HalfAPI class clean and rewrite 2021-11-22 18:18:06 +01:00
Maxime Alves LIRMM
ad9bd45ba0 [logging] use logger from halfapi.logging 2021-11-16 17:45:40 +01:00
Maxime Alves LIRMM
00c7b5caf4 [doc] update README and add joel to license 2021-11-16 17:45:40 +01:00
Maxime Alves LIRMM
5184ab4411 [ci][wip] utilisation de devtools/kaniko du lirmm 2021-11-16 17:40:05 +01:00
Maxime Alves LIRMM
159d38cb94 [wip][ci] utilisation image officielle buildah 2021-11-10 10:46:32 +01:00
Maxime Alves LIRMM@home
0cad726f8c [ci] try buildah 2021-11-01 10:34:52 +01:00
Maxime Alves LIRMM@home
94e09a546b [dockerfile] install git 2021-10-12 18:18:54 +02:00
Maxime Alves LIRMM@home
0643af5cca [dockerfile] add gunicorn/uvicorn 2021-10-12 18:08:07 +02:00
Maxime Alves LIRMM@home
339c910c86 [wip][ci] "before_script" runs before every job 2021-10-12 14:30:11 +02:00
Maxime Alves LIRMM@home
948372fcbc [wip][ci] try to build docker image 2021-10-12 03:18:32 +02:00
Maxime Alves LIRMM@home
5a0509a114 set loglevel even if in production 2021-10-12 03:10:10 +02:00
Maxime Alves LIRMM@home
c99e636d6e [0.5.13]
Squashed commit of the following:

commit 4552d85cc49fda572e54aa9c8054031554bfcb3a
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr>
Date:   Thu Oct 7 13:57:37 2021 +0200

    [0.5.13]

commit 38032acfac559155b31c12cf12673c81b7cfdf20
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr>
Date:   Thu Oct 7 13:57:12 2021 +0200

    add 503 error code

commit 6f516e844b0f3786aa571d1ac8d575247ff7b7fe
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr>
Date:   Thu Oct 7 13:26:15 2021 +0200

    [ci] add run halfapi --version
2021-10-07 13:58:03 +02:00
Maxime Alves LIRMM@home
8b88d7f1b4 [0.5.12] fix production no reload 2021-10-07 13:23:53 +02:00
Maxime Alves LIRMM@home
f27b68e350 [0.5.11] 2021-10-04 22:16:27 +02:00
Maxime Alves LIRMM@home
f3c12f516e [cli] run does not reload modules by default 2021-10-04 20:11:32 +02:00
Maxime Alves LIRMM
55109e271c [conf] fix HALFAPI_PROD variable that is set to a string 2021-09-03 16:38:20 +02:00
Maxime Alves LIRMM
51877b271e [conf] Read "HALFAPI_SECRET" file content if the option is not specified. Do not use HALFAPI_SECRET to store the *secret* value 2021-09-03 16:22:55 +02:00
Maxime Alves LIRMM
061c966072 [lib.domain] fix mounting domains with environment variable 2021-09-03 14:56:18 +02:00
Maxime Alves LIRMM
cdd2214043 [conf] halfapi becomes configurable only by environment variables (for one domain) 2021-09-03 13:17:06 +02:00
Maxime Alves LIRMM
0470f9fa89 [0.5.9] release
Squashed commit of the following:

commit 7fe3e22f5e4108b5eb149abf8d608334debc49ca
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Thu Sep 2 16:59:52 2021 +0200

    [0.5.9] release

commit c36c0fcc982388a5acf2f9f937fa8ab54a18f3de
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Thu Sep 2 16:53:13 2021 +0200

    [conf] fix #19 et ajout du test (test_dummy_project_router/test_get_config)

    configuration du domaine accessible depuis :

    l'attribut config de l'argument "halfapi" pour les fonctions
    request.scope['config'] pour les fonctions async

commit cc235eee8c6f8f5d3606dda0f88156697eac296e
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Thu Sep 2 14:59:17 2021 +0200

    [tests] don't import click two times

commit fa418478c76205bb407e536737d8e389b4bf391c
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Thu Sep 2 14:57:06 2021 +0200

    [clean] remove unused variables, remove [] as default value in fct, raise from exc
2021-09-02 17:01:36 +02:00
Maxime Alves LIRMM
74b79120ba [deps] starlette 0.16 2021-09-02 14:48:48 +02:00
Maxime Alves LIRMM
bc556854ac Fix #21 by simplifying DomainMiddleware
Tests are passing, but we loose the by-domain configuration (#19)

Squashed commit of the following:

commit d75fafcb9a043ac2540b2ac135704721b002d3c0
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Thu Sep 2 14:40:05 2021 +0200

    fix #21

commit 38c59e4ea3b40bd230f2add2bb0e05772913c097
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Thu Sep 2 01:13:51 2021 +0200

    [deps] starlette 0.15 (breaks tests)

    FAILED tests/test_debug_routes.py::test_current_user - AttributeError: 'DomainMiddleware' object has no attribute 'call_next'
    FAILED tests/test_debug_routes.py::test_log - AttributeError: 'DomainMiddleware' object has no attribute 'call_next'
    FAILED tests/test_debug_routes.py::test_error - AttributeError:
    'DomainMiddleware' object has no attribute 'call_next'
    FAILED tests/test_dummy_project_router.py::test_get_route - AttributeError: 'DomainMiddleware' object has no attribute 'call_next'
    FAILED tests/test_dummy_project_router.py::test_delete_route - AttributeError: 'DomainMiddleware' object has no attribute 'call_next'
    FAILED tests/test_lib_schemas.py::test_get_api_routes - AttributeError: 'DomainMiddleware' object has no attribute 'call_next'
    FAILED tests/test_lib_schemas.py::test_get_schema_route - AttributeError: 'DomainMiddleware' object has no attribute 'call_next'
    FAILED tests/test_lib_schemas.py::test_get_api_dummy_domain_routes - AttributeError: 'DomainMiddleware' object has no attribute 'call_next'
2021-09-02 14:45:06 +02:00
Maxime Alves LIRMM
865a4dffd1 [ci] add continuous integration
Squashed commit of the following:

commit e7b303310f3726c4ec5b36db668f7a5aaee29287
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Thu Sep 2 10:35:16 2021 +0200

    fix gitlab-ci run

commit 360c03371f1b088ce883646799f829a50f7a04a8
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Thu Sep 2 08:26:51 2021 +0000

    Update .gitlab-ci.yml file
2021-09-02 10:38:19 +02:00
Maxime Alves LIRMM
5e2ccceedf [deps] "build" (python -m build --sdist; python -m build --wheel) 2021-09-02 10:20:03 +02:00
Maxime Alves LIRMM
844f6a8f14 [packaging] 0.5.8 update to PyPi 2021-09-01 15:59:47 +02:00
Maxime Alves LIRMM
43b7fe21df [license] change author 2021-09-01 15:26:56 +02:00
Maxime Alves LIRMM
c9ba99c1df [readme] fix formatting 2021-09-01 15:24:53 +02:00
Maxime Alves LIRMM
18a1f71d99 [package] set source url to github 2021-09-01 15:24:21 +02:00
Maxime Alves LIRMM
c2cea298bf [readme] update with instructions 2021-09-01 15:19:51 +02:00
Maxime Alves LIRMM
387fc01f44 [0.5.7] release, real fix for halfapi/acl route bug 2021-07-06 11:30:30 +02:00
Maxime Alves LIRMM
c2054e9aa9 [lib.domain.domain_acls] fix ROUTES constant using read_router function 2021-07-06 11:28:23 +02:00
Maxime Alves LIRMM@home
0e669b81b0 [domain] do not raise exception when the router is missing the ROUTES variable in halfapi/acls route 2021-06-30 15:13:57 +02:00
Maxime Alves LIRMM@home
b45c0bf746 [deps] Avoid starlette update to 0.15, stay in latest 0.14 2021-06-28 12:09:56 +02:00
Maxime Alves LIRMM@home
c920531610 [release] 0.5.6 2021-06-25 12:21:40 +02:00
Maxime Alves LIRMM@home
bb50fae186 [lib.acl] fixes #20 2021-06-25 12:21:10 +02:00
Maxime Alves LIRMM
0c3aeb532f [0.5.5] retablissement de la configuration par domaine 2021-06-23 15:04:37 +02:00
Maxime Alves LIRMM
8a9f93b9e0 [conf] lecture des sections du nom des domaines activés et ajout dans request.scope['config']['config'] 2021-06-23 15:03:00 +02:00
Maxime Alves LIRMM
9381e1582e [0.5.4] 2021-06-18 17:11:34 +02:00
Maxime Alves LIRMM
a539212faf [lib.acl] fix args_check for get parameters 2021-06-18 12:08:51 +02:00
Maxime Alves LIRMM
a14285475e [lib.domain] check if an HTTPException is raised, else raise an HTTPException(500) 2021-06-18 10:39:05 +02:00
Maxime Alves LIRMM@home
15d69efd60 [tests] /halfapi/schema 2021-06-17 19:14:39 +02:00
Maxime Alves LIRMM@home
2819483070 [doc] docstring for api_routes 2021-06-17 19:02:11 +02:00
Maxime Alves LIRMM@home
81f6cf8b39 [0.5.3]
Squashed commit of the following:

commit ac935db6d6
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr>
Date:   Thu Jun 17 18:52:49 2021 +0200

    [tests] remove dummy-domain from dependencies

commit 4d50363c9b
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr>
Date:   Thu Jun 17 18:52:18 2021 +0200

    [tests] update tests for 0.5.3

commit 6181592692
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr>
Date:   Thu Jun 17 18:17:51 2021 +0200

    [lib.*] Refactor libs

commit ed7485a8a1
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr>
Date:   Thu Jun 17 18:15:10 2021 +0200

    [app] Use HalfAPI class to be able to use custom configuration
    à

commit fa1ca6bf9d
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Wed Jun 16 15:34:25 2021 +0200

    [wip] tests dummy_domain

commit 86e8dd3465
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Tue Jun 15 18:12:13 2021 +0200

    [0.5.3] ajout de la config actuelle dans les arguments des routes

commit aa7ec62c7a
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Tue Jun 15 11:16:23 2021 +0200

    [lib.jwtMw] verify signature even if halfapi is in DEBUG mode

commit e208728d7e
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Tue Jun 15 10:49:46 2021 +0200

    [lib.acl] args_check doesn't check required/optional arguments if "args" is not specified in request, if the target function is not async

commit aa4c309778
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Tue Jun 15 09:45:37 2021 +0200

    [lib.domain] SUBROUTER can be a path parameter if including ":"

commit 138420461d
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr>
Date:   Tue Jun 15 07:24:32 2021 +0200

    [gitignore] *.swp

commit 0c1e2849ba
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr>
Date:   Tue Jun 15 07:24:14 2021 +0200

    [tests] test get route with dummy projects

commit 7227e2d7f1
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr>
Date:   Mon Jun 14 17:18:47 2021 +0200

    [lib.domain] handle modules without ROUTES attribute

commit 78c75cd60e
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr>
Date:   Mon Jun 14 16:34:58 2021 +0200

    [tests] add dummy_project_router tests for path-based routers (without ROUTES variable)
2021-06-17 18:53:23 +02:00
Maxime Alves LIRMM
eb68d06ac0 [0.5.2] /halfapi/acls crash if acl attribute does not exist on a route 2021-06-07 19:40:50 +02:00
Maxime Alves LIRMM
3fb6fb4ded [0.5.1] Reactivation de la route "/halfapi/acls" (bug lors de multiples de domaines) 2021-06-07 19:30:00 +02:00
Maxime Alves LIRMM
e5a278c84c [release] 0.5.0 2021-06-07 16:44:43 +02:00
Maxime Alves LIRMM
904783b134 [deps] 2021-05-28 22:30:39 +02:00
Maxime Alves LIRMM
e0b06f51d5 [acl decorator] when check flag is activated, returns a 200 status code with empty body if no acl has passed
check function should always return 200. If we don't do this, we can't see the difference between a "refused" call and a "refused" check
2021-05-28 22:23:20 +02:00
Maxime Alves LIRMM
8ca94ab7ed [acl decorator] possibility to decorate functions with "@route_acl_decorator" 2021-05-28 22:22:05 +02:00
Maxime Alves LIRMM
e4e04c6ac1 [JWTmw] add CheckUser to be used when using the "check" flag. Add "user_id" query param to check access of a specific user to a route 2021-05-28 22:18:58 +02:00
Maxime Alves LIRMM
0e5a8ede9d [test] test of lib.acl 2021-05-28 22:13:48 +02:00
Maxime Alves LIRMM
a0c41d7d78 [tests] jwt, use of TestClient instead of requests 2021-05-28 22:12:53 +02:00
Maxime Alves LIRMM
a82fd6def0 [acl] ajout de l'acl private (test purposes) 2021-05-28 22:02:35 +02:00
Maxime Alves LIRMM
9a9bc16bbc [tests] déplacement des fixtures jwt dans conftest 2021-05-28 21:54:45 +02:00
Maxime Alves LIRMM
ea1f54cb82 [tests] JWTmw - flags check et user_id 2021-05-28 12:41:23 +02:00
Maxime Alves LIRMM
3c6713b5e2 [tests] add JWTMw test for query without tokens (must return Unauth user) 2021-05-28 12:27:35 +02:00
Maxime Alves LIRMM
c7e29e399b [tests] use "isinstance" instead of "type" 2021-05-28 12:26:51 +02:00
Maxime Alves LIRMM
89a5f3aa52 [tests] nettoyage test jwt 2021-05-28 12:26:27 +02:00
Maxime Alves LIRMM@home
933f456c86 Montage des routes uniquement si les variables DOMAINDICt et SECREt sont présentes 2021-04-27 08:26:48 +02:00
Maxime Alves LIRMM@home
5276833afe [lib/domain] ajout du dossier courant dans le path pour import de librairies dans le dossier du projet 2021-04-24 23:54:18 +02:00
Maxime Alves LIRMM@home
10b1960f4e nettoyage / commentaires / renommage de variables - JWTUser.id deviens JWTUser.identity 2021-04-24 08:56:18 +02:00
Maxime Alves LIRMM@home
a2fb70f84b DomainMiddleware ne vérifie plus les domaines non concernés par la première partie du path (optimisation vitesse) 2021-04-23 12:51:51 +02:00
Maxime Alves LIRMM@home
795ca3dcc0 [decorator] ajout du décorateur "args_check" pour automatiser la vérification des args requis/optionels 2021-04-22 23:36:34 +02:00
Maxime Alves LIRMM@home
5b67d938e2 Ajout du timing middleware pour le mode debug 2021-04-22 22:33:02 +02:00
Maxime Alves LIRMM@home
89ec439d3e ajout du header x-args-required et x-args-optional 2021-04-22 22:06:27 +02:00
Maxime Alves LIRMM@home
b5ef4a12d1 Ajout du header http "x-acl" qui renseigne l'acl qui a passé dans une requête 2021-04-22 22:04:21 +02:00
Maxime Alves LIRMM@home
4eb23fd189 Ajout de l'option "check" permettant de connaître l'acl qui passe 2021-04-22 17:30:21 +02:00
Maxime Alves LIRMM@home
e1c3d61207 Rajout de l'argument "doc" sur la route /halfapi/acls 2021-04-22 11:45:12 +02:00
Maxime Alves LIRMM@home
cd0df35496 [tests][lib/responses] format des dates ORJSON + update deps (fix bug dates) 2021-03-12 19:02:23 +01:00
Maxime Alves LIRMM@home
607a288e28 [debug] ajout des routes /error/{code:str} et /exception 2021-03-12 18:59:30 +01:00
Maxime Alves LIRMM@home
a3d546905c [update] fixage des versions des dépendances, fixage de python >= 3.7 2021-01-26 01:04:05 +01:00
Maxime Alves LIRMM@home
fecdaa29e5 [update] mise à jour du Pipfile.lock et de jwt_middleware
Upgrade de toutes les dépendances
Suppression de la dépendance à jwt
Tests : ok
2021-01-26 00:58:59 +01:00
Joël Maïzi
f36a2d8e06 [ORJSONResponse] Handle sets. 2021-01-25 10:44:07 +01:00
Maxime Alves LIRMM@home
54e215b6ff [test][nf] added basic test methods for orjsonresponse 2021-01-23 09:31:35 +01:00
Joël Maïzi
a5300962ad [responses] cast set to list. 2021-01-23 08:49:07 +01:00
Maxime Alves LIRMM
d21ee175e9 [responses] cast special types in ORJSONResponse 2021-01-14 10:55:02 +01:00
Maxime Alves LIRMM@home
f8e546007c [conf] lower loglevel string 2020-12-04 18:12:42 +01:00
Maxime Alves LIRMM@home
8d414f2bdd [config] ajout de l'option LOGLEVEL (info|debug|warning...) 2020-12-04 10:13:54 +01:00
Maxime Alves LIRMM@home
e61dd7eeaa [logs] change level de certains messages 2020-12-03 18:49:37 +01:00
Maxime Alves LIRMM@home
f4ba64f186 [debug] added /halfapi/log as a debug route that generates all type of logs 2020-11-12 17:56:05 +01:00
Maxime Alves LIRMM@home
d4a6bb1a04 [domainMW] set "acl_pass" variable into scope 2020-11-09 21:27:23 +01:00
Maxime Alves LIRMM@home
5d4b8d38b4 0.3.1 2020-11-09 21:26:31 +01:00
Maxime Alves LIRMM@home
9516eaa6d7 RAISES when a router import fails - test your domains routers FFS 2020-11-04 17:02:19 +01:00
Maxime Alves LIRMM@home
56657036e4 Added logging for acls 2020-11-04 13:32:33 +01:00
Maxime Alves LIRMM@home
73d49031a7 handle configuration when domain does not exist 2020-11-04 12:31:11 +01:00
Maxime Alves LIRMM@home
4782764059 add request.scope['config'] when there is a section with the domains name in the project's configuration 2020-11-04 05:01:26 +01:00
Maxime Alves LIRMM@home
61aec6871a [release][v0.3.0] Updated version to 0.3.0 2020-10-31 17:48:55 +01:00
Maxime Alves LIRMM@home
9a4f90d36b /lib/domain - try for ImportError bug 2020-10-27 13:07:49 +01:00
Maxime Alves LIRMM
64e60343bf [conf] Clean of DOMAINS global variable 2020-10-27 13:07:49 +01:00
Maxime Alves LIRMM
4df34b5d87 logging 2020-10-27 13:07:49 +01:00
Maxime Alves LIRMM
24c68b51f2 [lib] ORJSONResponse can now accept specific encoders 2020-10-26 15:12:03 +01:00
Maxime Alves LIRMM
d31efe3cc4 [deps] removed half_orm from dependencies 2020-10-20 08:25:50 +02:00
Maxime Alves LIRMM
e590bc31fe [doc] lib/domain, lib/domain_middlware 2020-10-07 16:13:54 +02:00
Maxime Alves LIRMM
3959e6d614 [tests] add dummy_domain as a testing dependency (because domains can't
be "not installed" in sys.path)
2020-10-07 15:51:24 +02:00
Maxime Alves LIRMM
1e1ff2fb69 [routes] skip acls that are falsy 2020-10-07 15:50:50 +02:00
Maxime Alves LIRMM
63b73a2bc1 [middleware][domain] load api and acl objects at each call (TODO: use a cache) 2020-10-07 15:49:49 +02:00
Maxime Alves LIRMM
710d390b49 [cli][conf] suppression of BASE_DIR option (now defaults to os.getcwd) 2020-10-07 15:48:48 +02:00
Maxime Alves LIRMM
cb5724b4fa [doc] lib/domain_middleware 2020-10-07 13:37:31 +02:00
Maxime Alves LIRMM
23bd876c4c [conf] fix list comprehension 2020-10-07 09:55:03 +02:00
Maxime Alves LIRMM
40547ddf30 [tests][OK] Using a dummy_domain, adding it and listing it's routes 2020-10-07 09:55:03 +02:00
Maxime Alves LIRMM
f5ebabbcd4 [refactor] update to avoid using global variables, and configuration are not stored in /etc/half_api anymore
Added domain_middleware
2020-10-07 09:55:03 +02:00
Maxime Alves LIRMM
781736c151 [logs] print -> logger lib.domain 2020-10-07 09:55:03 +02:00
Maxime Alves LIRMM
acb0a46904 [deps] add PyYAML to dependencies 2020-10-07 09:55:03 +02:00
Maxime Alves LIRMM
b651d90f0e [cli] add entry point to be just "halfapi" in Pipfile (__main__.py) 2020-10-07 09:55:03 +02:00
Maxime Alves LIRMM
0a34948b98 [cli] switch halfapi import to relative imports in cli.py 2020-10-07 09:55:03 +02:00
Maxime Alves LIRMM
c41d0d8f8f [deps] jwt->PyJWT in Pipfile 2020-10-07 09:54:31 +02:00
Maxime Alves LIRMM
91ea25791b [doc] lib/domain.py annotation and comments 2020-10-07 09:54:09 +02:00
Maxime Alves LIRMM
0cd7c987e5 pylint lib/acl.py 2020-10-07 09:54:09 +02:00
Joël Maïzi
6653a0cd0a Fix wrong dependency jwt -> pyjwt. 2020-10-05 11:39:55 +02:00
Maxime Alves LIRMM
2d51f260cd [clean] remove cli/lib dir 2020-10-05 11:33:28 +02:00
Maxime Alves LIRMM
8fc1ba6c91 [doc] cli/domain.py 2020-10-05 11:31:53 +02:00
Maxime Alves LIRMM
9753f4be95 [doc] cli/run.py 2020-10-05 11:30:18 +02:00
Maxime Alves LIRMM
584e0b6584 [doc] cli/run.py 2020-10-05 11:29:17 +02:00
Maxime Alves LIRMM
82e2ccbdbc [tests] fix tests according to abfaf2e1 2020-10-05 11:28:50 +02:00
Maxime Alves LIRMM
a6f2187032 [doc][style] cli/init.py 2020-10-05 11:23:44 +02:00
Maxime Alves LIRMM
1ee5da1588 [tests][jwt] fixs tests according to abfaf2e1 2020-10-05 11:18:47 +02:00
Maxime Alves LIRMM
1d0d3563cb [style] Clean cli/__init__.py 2020-10-05 11:10:34 +02:00
Maxime Alves LIRMM
91581f9ae6 [docs][style] cli/config.py 2020-10-05 11:10:00 +02:00
Maxime Alves LIRMM
6ac0b5acd9 [doc] cli/cli.py 2020-10-05 11:08:27 +02:00
Maxime Alves LIRMM
77598b8453 [doc][style] document fonctions and fix variable names 2020-10-05 11:04:32 +02:00
Maxime Alves LIRMM
ad1fed7117 [doc] add docstring to conf.py 2020-10-05 10:57:40 +02:00
Maxime Alves LIRMM
59ed5884ce [style] rename exceptions (e) as exc (pep8 style) 2020-10-05 10:55:55 +02:00
Maxime Alves LIRMM
5c4e81d5d2 [jwt_mw] Refuse DEBUG tokens in PROD mode with websockets 2020-10-05 10:55:55 +02:00
Maxime Alves LIRMM
21950aa6cd [cli] remove lib/db 2020-10-05 10:55:55 +02:00
Joël Maïzi
abfaf2e1ea [acls] Fix wrong key in payload for decoded JWT. 2020-10-05 10:48:00 +02:00
Maxime Alves LIRMM
333aca9e2c [app] clean code 2020-10-05 10:09:01 +02:00
Maxime Alves LIRMM
b6e511a96d [lib][domain/routes] clean code, use "yield from" 2020-10-04 18:09:23 +02:00
Maxime Alves LIRMM
deb41be3e8 [deps] added pylint as devdep 2020-10-04 17:27:24 +02:00
Maxime Alves LIRMM
429b26dec6 clean code + logs in conf 2020-10-04 17:27:02 +02:00
Maxime Alves LIRMM
c603727190 [test] adjust test_gen_router_routes to fit the new behaviour 2020-10-04 17:26:36 +02:00
Maxime Alves LIRMM
74e0b3dc54 [acls] treat decorator as acl in route decorator 2020-09-30 10:59:03 +02:00
Joël Maïzi
31878d971e [routes] Add halfapi routes in production. 2020-09-30 10:31:52 +02:00
Maxime Alves LIRMM
b89e03746f [acls] Allows decorators to be used as acls (fct=acl.public). 2020-09-30 10:31:25 +02:00
Maxime Alves LIRMM
79210e503e [acls] /halfapi/acls posé 2020-09-30 08:07:55 +02:00
Maxime Alves LIRMM
360f59b6ba [schema] fixes non-str keys in ORJSONResponse 2020-09-29 15:29:54 +02:00
Joël Maïzi
7d6bc2c181 [router][api] Fix GET / 2020-09-29 15:08:35 +02:00
Joël Maïzi
0a94f71dad [routing] Fix api representation (remove duplicates and fct). 2020-09-29 13:51:54 +02:00
Maxime Alves LIRMM
e223c0791c [routing] fix route description when there is multiple verbs 2020-09-29 11:12:45 +02:00
Joël Maïzi
d93fb23bba [0.2.1] Allows route parameters without acls. The route is deactivated. 2020-09-29 09:52:20 +02:00
Maxime Alves LIRMM
3530f53820 [routing] handle fqtn parameter in domain schema + get_api_routes (/) + /user -> /halfapi/current_user 2020-09-28 17:22:27 +02:00
Maxime Alves LIRMM@home
f0d980e035 [0.2.0] Remove any reference to databases, clean clode, doc 2020-09-25 01:06:57 +02:00
Maxime Alves LIRMM@home
246c9224e3 [tests] all tests updated to fit new fixture 2020-09-25 01:06:21 +02:00
Maxime Alves LIRMM@home
70723ea580 [tests] add acl.py to dummy_domain 2020-09-25 01:05:27 +02:00
Maxime Alves LIRMM@home
4ef7ae377a [deps] add orjson dependency 2020-09-24 20:48:52 +02:00
Maxime Alves LIRMM@home
6a81c61649 [cli][domain] Re-implementation of list_routes, using .lib.schemas, add DOMAINSDICT constant in configuration, set a lot of default configurations 2020-09-24 19:49:16 +02:00
Maxime Alves LIRMM
2ad0b3a14b [doc][route] /schema - implementation of doc generation 2020-09-23 15:23:36 +02:00
Maxime Alves LIRMM
2610d9f089 forgot to import typ 2020-09-23 11:15:53 +02:00
Maxime Alves LIRMM
65797873da [lib][responses] include new responses in __all__ 2020-09-23 11:00:59 +02:00
Maxime Alves LIRMM
d6075de2eb [lib][responses] add ORJSONResponse and update HJSONResponse 2020-09-23 10:56:32 +02:00
Maxime Alves LIRMM
39d455b682 if the route begin with a "/" we remove it 2020-09-22 16:11:32 +02:00
Maxime Alves LIRMM
a78e6ebc75 [wip][routing] get_fct_name update 2020-09-22 15:46:58 +02:00
Maxime Alves LIRMM
5f7d66d4d6 [wip][routing] updated lib.domain function names 2020-09-22 14:54:45 +02:00
Maxime Alves LIRMM
38798549f6 [wip][app] adapt code to new router functions 2020-09-22 14:11:00 +02:00
Maxime Alves LIRMM
ba44a01a45 [wip][routing] update routes scanner functions in lib.routes 2020-09-22 14:04:19 +02:00
Maxime Alves LIRMM
c54101c3e6 [wip][routing] add routing functions in /lib/domains 2020-09-22 12:57:36 +02:00
Maxime Alves LIRMM
7337246fc1 [lib][routes] fix dict construction 2020-09-21 11:26:51 +02:00
Maxime Alves LIRMM
142ea24630 [routing] add the ACL name that returned True 2020-09-18 15:31:36 +02:00
Maxime Alves LIRMM
bcc4a3e9d8 [routing] gives "keys" of the Acl row to the endpoint as kwargs 2020-09-18 11:59:44 +02:00
Maxime Alves LIRMM
e8ed06a9b6 update api.sql with acl.keys 2020-09-16 14:18:37 +02:00
Joël Maïzi
31a8f68a0a [cli][domain] Remove misplaced elif in add_acls. 2020-09-16 12:21:52 +02:00
Maxime Alves LIRMM
c97fa3b4c2 [acls] added "fields" value for acl table 2020-09-16 11:46:45 +02:00
Maxime Alves LIRMM
87cc59849a [acl] pass path_params arguments to next function as kwargs 2020-08-31 13:08:03 +02:00
Maxime Alves LIRMM@home
1b40b95d19 [auth] fix bug when "debug flag" = False in token 2020-08-27 18:09:48 +02:00
Maxime Alves LIRMM@home
51722b73f8 [package] fixed the "scripts" section of Pipfile 2020-08-27 17:47:58 +02:00
Maxime Alves LIRMM@home
d944d45bbf [auth] added "debug flag" check and wrote relative tests close #12 2020-08-27 17:45:23 +02:00
Maxime Alves LIRMM
ed54127c81 [cli] added the "config" command to the CLI, updated tests 2020-08-27 11:27:28 +02:00
Maxime Alves LIRMM
8d2be99068 [package] fixed some paths 2020-08-27 09:54:48 +02:00
Maxime Alves LIRMM
2389d67749 [conf] sets the HALFAPI_PROD environment variable 2020-08-26 14:06:53 +02:00
Maxime Alves LIRMM@home
30651ec19b [deps] jwt -> PyJWT 2020-08-07 02:08:22 +02:00
Maxime Alves LIRMM@home
bdc131434b [conf] set SECRET in environ 2020-08-07 01:58:41 +02:00
Maxime Alves LIRMM@home
3849ba4500 [cli][domain] one more thing 2020-08-07 00:30:59 +02:00
Maxime Alves LIRMM@home
05a95d069b [cli][domain] continue to convert code ... 2020-08-07 00:28:46 +02:00
Maxime Alves LIRMM@home
63793a94e5 [cli][domain] forgot to remove argument 2020-08-07 00:26:29 +02:00
Maxime Alves LIRMM@home
51c319de20 [cli][domain] continue -> return 2020-08-07 00:24:58 +02:00
Maxime Alves LIRMM@home
976ba9b808 [cli][domain] update_db is now run with a string as argument 2020-08-07 00:19:10 +02:00
Maxime Alves LIRMM@home
cdcf8d034d [cli][domain] clean debug code 2020-08-07 00:09:57 +02:00
Maxime Alves LIRMM@home
b4302f50e4 [conf] moved "domains" section to .halfapi/config 2020-08-07 00:02:59 +02:00
Maxime Alves LIRMM@home
c4872ec0b3 [cli][tests] Changed the routes names and wrote the tests according to current configuration 2020-08-07 00:00:33 +02:00
Maxime Alves LIRMM@home
ad1304f8d4 [tests][cli] first tests for post-init phase 2020-08-06 23:39:04 +02:00
Maxime Alves LIRMM@home
7c4eafb40c [cli][domain] better output for list_routes 2020-08-05 21:48:29 +02:00
Maxime Alves LIRMM@home
63bd4c8db6 [domain] changed "route" command to "domain", fixed "list_routes" 2020-08-05 21:26:59 +02:00
Maxime Alves LIRMM
914cb149e1 [tests][cli] fixed cli import 2020-08-05 14:28:55 +02:00
Maxime Alves LIRMM
6a65aaeaef [cli][tests] split cli __init__ module, added check for .halfapi/config 2020-08-05 14:01:39 +02:00
Maxime Alves LIRMM
155bab1e8f [cli] split cli/__init__ into multiple files 2020-08-05 14:01:39 +02:00
Maxime Alves LIRMM
60877762c0 [cli] moved run command to its own file 2020-08-05 14:01:39 +02:00
Joël Maïzi
d164ad001a [lib] Add HJSONResponse. 2020-08-05 12:58:05 +02:00
Maxime Alves LIRMM
446db4ee27 [cli] renamed init-project to init 2020-08-05 12:30:11 +02:00
Maxime Alves LIRMM
7ff8d803ee [tests][cli] added tests for domain command 2020-08-05 12:28:37 +02:00
Maxime Alves LIRMM
048d06de37 [tests] moved cli tests to one file with incremental tests (exits if one fails) 2020-08-05 12:28:37 +02:00
Maxime Alves LIRMM
d47f735828 [tests][cli] init_project tests passes 2020-08-05 12:28:37 +02:00
Maxime Alves LIRMM
50314e6656 [tests][cli] rewrite of cli/test_init_project.py 2020-08-05 12:28:37 +02:00
Joël Maïzi
7f0e2d0a07 [cli] Print routes when running application. 2020-08-05 10:34:54 +02:00
Joël Maïzi
ceaf2774bf [cli] .halfapi/domains. 2020-08-05 10:04:20 +02:00
Joël Maïzi
15a63f3713 [cli] create project directory. 2020-08-05 09:40:16 +02:00
Joël Maïzi
d86dab8de1 Add requirements versions. 2020-08-05 09:32:00 +02:00
Joël Maïzi
36f87849ca Remove gitpy2 dependency. 2020-08-05 09:24:02 +02:00
Maxime Alves LIRMM
b34631cdd0 [cli][init-project][tests][wip] creation du projet halfapi 2020-08-05 09:24:02 +02:00
Maxime Alves LIRMM
74c2c4e056 [tests] updated tests for init-project 2020-08-05 09:24:02 +02:00
Joël Maïzi
a3c3d7c816 [cli][WIP] Init API repo directory. 2020-08-05 09:24:02 +02:00
Joël Maïzi
14ab8bd346 [routes] Await route function in decorator. 2020-07-30 11:11:40 +02:00
Joël Maïzi
b8f1d3a35e [routes] Fix func -> fct. 2020-07-30 11:06:36 +02:00
Joël Maïzi
f28e11e051 [deps] Remove cli from extras_require. 2020-07-30 10:03:04 +02:00
Maxime Alves LIRMM@home
4eba987eb9 [cli] simplification of halfapi routes 2020-07-29 22:58:25 +02:00
Maxime Alves LIRMM@home
3a81dfae96 [tests] dbupdate removal 2020-07-29 22:10:15 +02:00
Maxime Alves LIRMM@home
d5fcfa0f24 [doc] Update readme 2020-07-29 22:07:16 +02:00
Maxime Alves LIRMM@home
7ef6e78010 [deps] Updated dependencies and added files for standard packaging.
The halfapi-cli tool needs to be installed using [cli] option :
pip3 install -e .[cli]
2020-07-29 22:07:16 +02:00
Maxime Alves LIRMM@home
4d02cf4acd [cli] Added init-project command and cli tests 2020-07-29 22:07:16 +02:00
Joël Maïzi
527d5c2e93 Replace dbupdate command with routes.
Allows to use --update, --list, ...
2020-07-29 16:40:25 +02:00
Maxime Alves LIRMM@home
e289f6ad6b [cli] handle dbupdate without argument 2020-07-28 16:22:34 +02:00
Joël Maïzi
be1e5ed722 Fix some typos. 2020-07-28 14:40:01 +02:00
Maxime Alves LIRMM@home
cf9b5168b4 [conf] get -> items for list of domains, fix default conf 2020-07-28 11:50:53 +02:00
Joël Maïzi
f5a210a855 Fix #9. Mise en place du chargement des domaines à partir du fichier de conf. 2020-07-28 11:04:21 +02:00
Maxime Alves LIRMM@home
9e4c3506d5 [conf] fix default config 2020-07-28 09:54:29 +02:00
Maxime Alves LIRMM@home
ad3792340c cleaned trailing spaces 2020-07-28 09:46:21 +02:00
Maxime Alves LIRMM
e4d0b6c17e handle "/" routes 2020-07-24 18:27:20 +02:00
Maxime Alves LIRMM
0fffab106f read correct config from halfapiconfig 2020-07-24 18:16:57 +02:00
Maxime Alves LIRMM
7f3fd74a31 read project name from .halfapiconfig 2020-07-24 18:05:58 +02:00
Maxime Alves LIRMM
98f0536a1a moved acl.py in lib 2020-07-24 17:45:41 +02:00
Maxime Alves LIRMM
c68dfda96c [wip] split conf and database variables into files 2020-07-24 17:41:25 +02:00
Maxime Alves LIRMM
0282da6e3d [release][0.1.0] First HalfAPI release
Squashed commit of the following:

commit 68032dc55a07565ccd17a188407d9ac2537b62e6
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Thu Jul 23 15:40:26 2020 +0200

    [release][0.1.0] First HalfAPI release

commit a046a81114a3ae22bbc84a53f1e85217a0954dbc
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Thu Jul 23 15:26:15 2020 +0200

    [doc] màj du readme

commit ed45b3011125c071aa53df8087d28bfa1150b373
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Thu Jul 23 11:03:17 2020 +0200

    [wip] rm apidb

commit 7df4b9bacf3d26f09ea07856587505f74284cab4
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Thu Jul 23 10:58:46 2020 +0200

    [db] forgot foreign key router->domain

commit 604b9a90f405121725e4b2126d73a5a83eec398f
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Thu Jul 23 10:51:01 2020 +0200

    [wip] routes mounting fixed

commit c50a5572633d7dcc3cad48ef79c5ea1ca098284d
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Thu Jul 23 10:29:09 2020 +0200

    [wip][db][nf] http_verb, fct_name are in api.route

commit 2b5b78db2f9c280dd5eb9d8bafc9174d9afc092f
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Wed Jul 22 17:37:21 2020 +0200

    [wip] refactor du 22 juillet 2222

commit d019b7e333ab37f106895c84521cef1bfe768caa
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Wed Jul 22 14:48:51 2020 +0200

    [wip] remove "version" from app

commit 98ccd61dcf369b8c4aac817a0c8409b6fed00f50
Author: Joël Maïzi <joel.maizi@lirmm.fr>
Date:   Wed Jul 22 14:45:28 2020 +0200

    Remove api.version from database.

commit aa0d4f8dbba8b8ec878835bb58b69facff7e675e
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr>
Date:   Tue Jul 21 21:46:22 2020 +0200

    [db] added router as api.acl primary_key part

commit a97984e9de0e6a00bddca7dece0c715c9c16cbe1
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr>
Date:   Tue Jul 21 21:41:31 2020 +0200

    [wip] moved all acl treatment to acl_caller_middleware, fix route mounting, fix typo in logs

commit 3dd310e80aaf6cb32f6c4ac23c1e2a924cebfde1
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Tue Jul 21 13:10:26 2020 +0200

    [wip][nf] gestion des routes avec routers

commit c2687c4a24126fbc3e57257bf23c267b334df522
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Tue Jul 21 12:39:53 2020 +0200

    [db] ajout de la table api.router

commit 9a10f76cf7790f75f23b72e19b0a58978752565c
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Tue Jul 21 12:19:53 2020 +0200

    [db] renommage des champs d'acl

commit c4e8c26a24835559d2e9b251df0eb462fe7e667d
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Tue Jul 21 12:13:38 2020 +0200

    [wip][nf] modification du systeme de montage des routes

commit b7e8352ba1e427e9883797a44bb0f3da5edd576d
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Tue Jul 21 11:15:30 2020 +0200

    [wip][dbupdate] insertion des routes au nouveau format

commit 28947444c4c062e6ced74f9bfdef11a36ddc438b
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Tue Jul 21 10:32:57 2020 +0200

    [wip][dbupdate] Suppression des routes hors domaine
2020-07-23 17:54:47 +02:00
Maxime Alves LIRMM
5d9f4631be [poetry] update deps 2020-07-20 11:31:21 +02:00
Maxime Alves LIRMM
8930e065d2 [query] add fields list to query 2020-07-20 11:20:58 +02:00
Maxime Alves LIRMM
ceb0b075de [deps] added pyods3 for organigramme..., deleted CSVresponse from libs 2020-07-16 17:54:07 +02:00
Maxime Alves LIRMM
5bc97a2a58 [lib] remove CSVresponse from parse_query funct 2020-07-16 17:43:46 +02:00
Maxime Alves LIRMM@home
5debe56349 [doc] README systemd details 2020-07-15 00:11:31 +02:00
Maxime Alves LIRMM@home
c9fa127cd8 [etc] added system-configuration files for systemd and nginx 2020-07-14 23:39:09 +02:00
Maxime Alves LIRMM@home
f5caaf8b86 [doc] typo in README 2020-07-14 23:38:21 +02:00
Maxime Alves LIRMM@home
2b86a78a4e [doc] completed README 2020-07-14 15:14:23 +02:00
Maxime Alves LIRMM@home
ac2b00e200 [deps] added apidb to dependencies 2020-07-12 22:10:16 +02:00
Maxime Alves LIRMM@home
1aba6b5623 [cli] add apihost and apiport options to dbupdate 2020-07-12 22:00:52 +02:00
Maxime Alves LIRMM@home
32ed99deb9 [typo] domain -> Domain in cli.py 2020-07-12 20:25:39 +02:00
Maxime Alves LIRMM@home
6c5e64c202 [typo] fix true -> True 2020-07-12 19:38:53 +02:00
Maxime Alves LIRMM
9eead5cd85 [debug] better handling of debug routes 2020-07-10 12:58:53 +02:00
Maxime Alves LIRMM
1983383071 [conf] update code according to CONFIG in config.py 2020-07-10 12:57:40 +02:00
Maxime Alves LIRMM
a9b109d17b [conf] put CONFIG out of app.py, add some details to debug routes 2020-07-10 12:57:03 +02:00
Maxime Alves LIRMM
2f283db823 [acl] more checks in connected decorator 2020-07-10 12:53:25 +02:00
Maxime Alves LIRMM
1087804e8a [conf] clean configuration options 2020-07-09 12:07:39 +02:00
Maxime Alves LIRMM
7d2bb39593 [cli] fixed typo in booleans 2020-07-09 10:39:53 +02:00
110 changed files with 6841 additions and 1896 deletions

4
.gitignore vendored
View File

@ -137,3 +137,7 @@ dmypy.json
cython_debug/ cython_debug/
domains/ domains/
.vscode
# Vim swap files
*.swp

90
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,90 @@
# This file is a template, and might need editing before it works on your project.
# To contribute improvements to CI/CD templates, please follow the Development guide at:
# https://docs.gitlab.com/ee/development/cicd/templates.html
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Python.gitlab-ci.yml
# Official language image. Look for the different tagged releases at:
# https://hub.docker.com/r/library/python/tags/
image: python:alpine3.18
# Change pip's cache directory to be inside the project directory since we can
# only cache local items.
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
# REGISTRY_USERNAME - secret variable
# REGISTRY_PASSWORD - secret variable
# REGISTRY_SERVER - secret variable
IMAGE_NAME: ${REGISTRY_SERVER}/malves/${CI_PROJECT_NAME}
# Pip's cache doesn't store the python packages
# https://pip.pypa.io/en/stable/reference/pip_install/#caching
#
# If you want to also cache the installed packages, you have to install
# them in a virtualenv and cache it as well.
cache:
paths:
- .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:
- python3 -V # Print out python version for debugging
- python3 -m venv /tmp/venv
- /tmp/venv/bin/pip3 install .
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
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"
artifacts:
paths:
- /kaniko/.docker/config.json

248
CHANGELOG.md Normal file
View File

@ -0,0 +1,248 @@
# 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
- Configuration example files for systemd and a system-wide halfapi install
- Runs projects
- Handles JWT authentication middleware

11
Dockerfile Normal file
View File

@ -0,0 +1,11 @@
# syntax=docker/dockerfile:1
FROM python:alpine3.19
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

15
LICENSE.txt Normal file
View File

@ -0,0 +1,15 @@
Copyright (c) 2020-2021 Maxime ALVES <maxime@freepoteries.fr>, Joël Maïzi
<joel.maizi@collorg.org>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

7
MANIFEST.in Normal file
View File

@ -0,0 +1,7 @@
include pyproject.toml
# Include the README
include README.md
# Include the license file
include LICENSE

36
Pipfile Normal file
View File

@ -0,0 +1,36 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
pytest = ">=7,<8"
requests = "*"
pytest-asyncio = "*"
pylint = "*"
build = "*"
twine = "*"
pyflakes = "*"
vulture = "*"
virtualenv = "*"
httpx = "*"
openapi-schema-validator = "*"
openapi-spec-validator = "*"
coverage = "*"
[packages]
click = ">=8,<9"
starlette = ">=0.46,<0.47"
uvicorn = ">=0.13,<1"
orjson = ">=3.10,<4"
pyjwt = ">=2.10.0,<2.11.0"
pyyaml = ">=6.0.1,<7"
timing-asgi = ">=0.2.1,<1"
schema = ">=0.7.7,<1"
toml = ">=0.10,<0.11"
pip = "*"
packaging = ">=19.0"
python-multipart = "*"
[scripts]
halfapi = "python -m halfapi"

1254
Pipfile.lock generated Normal file

File diff suppressed because it is too large Load Diff

122
README.md
View File

@ -1,62 +1,104 @@
# halfAPI # HalfAPI
This Python-based ASGI application aims to provide the core functionality to Base tools to develop complex API with rights management.
multiple API domains.
It's developed at the [LIRMM](https://lirmm.fr) in Montpellier, France. This project was developped by Maxime Alves and Joël Maïzi. The name was chosen
to reference [HalfORM](https://github.com/collorg/halfORM), a project written by Joël Maïzi.
The name "halfAPI" comes from the deep relationship between it and
[halfORM](https://gite.lirmm.fr/newsi/halfORM), a project authored by
[Joël Maizi](https://gite.lirmm.fr/maizi).
## how-to ## Dependencies
As the project uses the [poetry]() package manager, you first have to install it globally. It will replace virtualenv, pip, etc... - python3
- python3-pip
`pip install poetry` - libgit2-dev
- starlette
Be sur to include the bin directory of pip in your PATH. - PyJWT
- click
- uvicorn
- orjson
- pyyaml
Then, cd in the halfapi repo, and chose your python version : ## Configuration
`poetry env use 3.8` Configure HalfAPI in the file : .halfapi/config .
- at this point, every time you come into the folder, when you launch something with "poetry run", it is launched within the virtual environment for this project - It's a **toml** file that contains at least two sections, project and domains.
And install the deps : https://toml.io/en/
`poetry install`
If you need a domain, i.e: organigramme, just add *-E organigramme* avec *install*.
`poetry install -E organigramme`
Then, to run the server :
`poetry run halfapi`
If you need to launch the test suite (only works if you have pytest installed) : ### Project
`poetry run pytest` The main configuration options without which HalfAPI cannot be run.
# API database **secret** : The file containing the secret to decode the user's tokens.
You just need to run the following command to insert the right data into the api database : **port** : The port for the test server.
`poetry run halfapi dbupdate` **loglevel** : The log level (info, debug, critical, ...)
# API database configuration
You can set the HALFORM_SECRET and HALFORM_DSN variables to setup the way you connect to the API database. ### Domains
HALFORM_SECRET="wtfqwertz" Specify the domains configurations in the following form :
HALFORM_DSN="dbname=api user=api password=api host=127.0.0.1 port=5432"
## Warning ```
[domains.DOMAIN_NAME]
name = "DOMAIN_NAME"
enabled = true
prefix = "/prefix"
module = "domain_name.path.to.api.root"
port = 1002
```
For the domains' databases, for now the database connection string is hardcoded (check app.mount_domains). Specific configuration can be done under the "config" section :
@TODO find a modular way to configure the database connection for each mounted domain ```
[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.
## Usage
Develop an HalfAPI domain by following the examples located in
tests/dummy_domain . An HalfAPI domain should be an importable python module
that is available in the python path.
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
```
### CLI documentation
Use the CLI help.
```
python -m halfapi --help
python -m halfapi domain --help
```
## API Testing
@TODO
### Example
Check out the [sample project](https://github.com/halfAPI/halfapi_sample_project)
that helps you to build your own domain.
## Development
@TODO

3
conf/env.mallirmm Normal file
View File

@ -0,0 +1,3 @@
DEV=1
DEBUG=1
DEBUG_ACL=public

3
conf/env.merles Normal file
View File

@ -0,0 +1,3 @@
DEV=1
DEBUG=1
DEBUG_ACL=public

20
conf/nginx/api Normal file
View File

@ -0,0 +1,20 @@
server {
listen 8080;
client_max_body_size 4G;
server_name api.lirmm.fr;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
proxy_buffering off;
proxy_pass http://uvicorn;
}
}
upstream uvicorn {
server unix:/var/lib/api/lirmm_api.sock;
}

View File

@ -0,0 +1,10 @@
GUNICORN_CMD_ARGS="--daemon \
--bind unix:/var/lib/halfapi/example_api.sock \
--max-requests 200 \
--max-requests-jitter 20 \
--workers 4 \
--log-syslog-facility daemon \
--worker-class uvicorn.workers.UvicornWorker
HALFORM_CONF_DIR=/etc/half_orm
HALFAPI_CONF_DIR=/etc/half_api

View File

@ -0,0 +1,19 @@
[Unit]
Description=HalfAPI - Project : Example API Service
Requires=halfapi_example_api.socket
After=network.target
[Service]
Type=simple
User=halfapi
Group=halfapi
WorkingDirectory=/var/lib/halfapi/example_api
EnvironmentFile=/etc/default/gunicorn/halfapi_example_api
ExecStart=/usr/bin/env gunicorn halfapi
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,13 @@
[Unit]
Description=HalfAPI - Project : Example API Socket
[Socket]
ListenStream=/var/lib/halfapi/example_api.sock
User=halfapi
SocketUser=halfapi
SocketGroup=www-data
# Optionally restrict the socket permissions even more.
# Mode=600
[Install]
WantedBy=sockets.target

View File

@ -1 +1,8 @@
__version__ = '0.0.0' #!/usr/bin/env python3
__version__ = '0.6.31'
def version():
return f'HalfAPI version:{__version__}'
if __name__ == '__main__':
print(version())

3
halfapi/__main__.py Normal file
View File

@ -0,0 +1,3 @@
from .cli.cli import cli
if __name__ == '__main__':
cli()

View File

@ -1,19 +0,0 @@
#!/usr/bin/env python3
from functools import wraps
""" Base ACL module that contains generic functions for domains ACL
"""
def connected(func):
""" Decorator that checks if the user object of the request has been set
"""
@wraps(func)
def caller(req, *args, **kwargs):
if not hasattr(req.user, 'is_authenticated'):
return False
return func(req, **kwargs)
return caller
def public(*args, **kwargs) -> bool:
"Unlimited access"
return True

View File

@ -1,124 +1,7 @@
#!/usr/bin/env python3 import os
# builtins from .halfapi import HalfAPI
import importlib from .logging import logger
import sys
from os import environ
# asgi framework def application():
from starlette.applications import Starlette from .conf import CONFIG
from starlette.middleware import Middleware return HalfAPI(CONFIG).application
from starlette.requests import Request
from starlette.responses import Response, JSONResponse
from starlette.routing import Route
from starlette.types import ASGIApp
from starlette.middleware.authentication import AuthenticationMiddleware
# typing
from typing import Any, Awaitable, Callable, MutableMapping
RequestResponseEndpoint = Callable[ [Request], Awaitable[Response] ]
# hop-generated classes
from .models.api.domain import Domain
# module libraries
from .lib.jwt_middleware import JWTAuthenticationBackend
from .lib.acl_caller_middleware import AclCallerMiddleware
from .lib.responses import *
def mount_domains(app: ASGIApp, domains: list):
""" Procedure to mount the registered domains on their prefixes
Parameters:
- app (ASGIApp): The Starlette instance
- domains (list): The domains to mount, retrieved from the database
with their attributes "version", "name"
Returns: Nothing
"""
for domain in domains:
if 'name' not in domain.keys() or 'version' not in domain.keys():
continue
# Retrieve domain app according to domain details
try:
print(f'Will import {domain["name"]}.app:app')
# @TODO 4-configuration
# Store domain-specific information in a configuration file
environ["HALFORM_DSN"] = "dbname=si user=si"
domain_mod = importlib.import_module(
f'{domain["name"]}.app')
domain_app = domain_mod.app
except ModuleNotFoundError:
sys.stderr.write(
f'Could not find module *{domain["name"]}* in sys.path\n')
continue
except ImportError:
sys.stderr.write(f'Could not import *app* from *{domain}*')
continue
except Exception as e:
sys.stderr.write(f'Error in import *{domain["name"]}*\n')
print(e)
continue
# Alter the openapi_url so the /docs page doesn't try to get
# /openapi.json (@TODO : report the bug to FastAPI)
domain_app.openapi_url = '/api/{version}/{name}/openapi.json'.format(**domain)
# Mount the domain app on the prefix
# e.g. : /v4/organigramme
try:
app.mount('/{version}/{name}'.format(**domain), app=domain_app)
except Exception as e:
print(f'Failed to mount *{domain}*\n')
def startup():
# This function is called at the instanciation of *app*
global app
# Mount the registered domains
try:
domains_list = [elt for elt in Domain().select()]
mount_domains(app, domains_list)
except Exception as e:
sys.stderr.write('Error in the *domains* retrieval\n')
raise e
async def root(request):
return JSONResponse({'payload': request.payload})
def check_conf():
if not environ.get('HALFORM_SECRET', False):
environ['HALFORM_SECRET'] = open('/etc/half_orm/secret').read()
print('Missing HALFORM_SECRET variable from configuration, seting to default')
CONFIG={
'DEBUG' : 'DEBUG' in environ.keys()
}
debug_routes = [
Route('/', lambda request, *args, **kwargs: PlainTextResponse('It Works!')),
Route('/user', lambda request, *args, **kwargs: JSONResponse({'user':str(request.user)})),
Route('/payload', lambda request, *args, **kwargs: JSONResponse({'payload':str(request.payload)}))
] if CONFIG['DEBUG'] is True else []
app = Starlette(
debug=CONFIG['DEBUG'],
routes=debug_routes,
middleware=[
Middleware(AuthenticationMiddleware, backend=JWTAuthenticationBackend(secret_key=environ.get('HALFORM_SECRET'))),
Middleware(AclCallerMiddleware),
],
exception_handlers={
401: UnauthorizedResponse,
404: NotFoundResponse,
500: InternalServerErrorResponse,
501: NotImplementedResponse
},
on_startup=[startup],
)

View File

@ -1,211 +0,0 @@
#!/usr/bin/env python3
# builtins
import click
import uvicorn
import os
import sys
import importlib
# database
import psycopg2
# hop-generated classes
from .models.api.version import Version
from .models.api.domain import Domain
from .models.api.route import Route
from .models.api.acl_function import AclFunction
from .models.api.acl import Acl
# module libraries
from .app import check_conf
HALFORM_DSN=''
HALFORM_SECRET=''
CONTEXT_SETTINGS={
'default_map':{'run': {'port': 8000}}
}
@click.group(invoke_without_command=True, context_settings=CONTEXT_SETTINGS)
@click.pass_context
def cli(ctx):
if ctx.invoked_subcommand is None:
return run()
@click.option('--envfile', default=None)
@click.option('--host', default='127.0.0.1')
@click.option('--port', default='8000')
@cli.command()
def run(envfile, host, port):
local_env = {}
if envfile:
try:
with open(envfile) as f:
print('Will use the following env parameters :')
local_env = dict([ tuple(line.strip().split('=', 1))
for line in f.readlines() ])
print(local_env)
except FileNotFoundError:
print(f'No file named {envfile}')
envfile = None
if 'DEV' in local_env.keys():
debug = True
reload = True
log_level = 'debug'
else:
reload = False
log_level = 'info'
click.echo('Launching application')
check_conf()
sys.path.insert(0, os.getcwd())
click.echo(f'current python_path : {sys.path}')
uvicorn.run('halfapi.app:app',
env_file=envfile,
host=host,
port=int(port),
log_level=log_level,
reload=reload)
@click.option('--dbname', default='api')
@click.option('--host', default='127.0.0.1')
@click.option('--port', default=5432)
@click.option('--user', default='api')
@click.option('--password', default='')
@click.option('--domain', default='organigramme')
@click.option('--drop', is_flag=true, default=false)
@cli.command()
def dbupdate(dbname, host, port, user, password, domain, drop):
def dropdb():
if not click.confirm(f'will now drop database {dbname}', default=true):
return false
conn = psycopg2.connect({
'dbname': dbname,
'host': host,
'port': port,
'user': user,
'password': password
})
cur = conn.cursor()
cur.execute(f'drop database {dbname};')
conn.commit()
cur.close()
conn.close()
return true
def delete_domain():
d = domain(name=domain)
if len(d) < 1:
return False
acl = Acl(domain=domain)
acl.delete()
fct = AclFunction(domain=domain)
fct.delete()
route = Route(domain=domain)
route.delete()
d.delete()
return True
def add_acl_fct(fct):
acl = AclFunction()
acl.version = version
acl.domain = domain
acl.name = fct.__name__
if len(acl) == 0:
acl.insert()
def add_acl(name, **kwargs):
acl = Acl()
acl.version = version
acl.domain = domain
acl.name = name
acl.path = kwargs['path']
acl.http_verb = kwargs['verb']
for fct in kwargs['acl']:
acl.function = fct.__name__
if len(acl) == 0:
if fct is not None:
add_acl_fct(fct)
acl.insert()
elif fct is None:
acl.delete()
def add_route(name, **kwargs):
print(f'Adding route {version}/{domain}/{name}')
route = Route()
route.version = version
route.domain = domain
route.path = kwargs['path']
if len(route) == 0:
route.insert()
def add_routes_and_acl(routes):
for name, route_params in routes.items():
add_route(name, **route_params)
add_acl(name, **route_params)
def add_domain():
new_domain = Domain(name=domain)
new_domain.version = version
if len(new_domain) == 0:
print(f'New domain {domain}')
new_domain.insert()
if drop:
dropdb()
delete_domain()
acl_set = set()
try:
# module retrieval
dom_mod = importlib.import_module(domain)
version = dom_mod.API_VERSION
add_domain()
# add main routes
ROUTES = dom_mod.ROUTES
add_routes_and_acl(dom_mod.ROUTES)
# add sub routers
ROUTERS = dom_mod.ROUTERS
for router_name in dom_mod.ROUTERS:
router_mod = importlib.import_module(f'.routers.{router_name}', domain)
add_routes_and_acl(router_mod.ROUTES)
except ImportError:
click.echo(f'The domain {domain} has no *ROUTES* variable', err=True)
except Exception as e:
click.echo(e, err=True)
if __name__ == '__main__':
cli()

28
halfapi/cli/cli.py Normal file
View File

@ -0,0 +1,28 @@
#!/usr/bin/env python3
"""
cli/cli.py Main entry point of halfapi cli tool
The init command is the only command loaded if not in a *project dir*, and it is
not loaded otherwise.
"""
# builtins
import click
@click.group(invoke_without_command=True)
@click.option('--version', is_flag=True)
@click.pass_context
def cli(ctx, version):
"""
HalfAPI Cli entry point
It uses the Click library
"""
if version:
from halfapi import version
click.echo(version())
from . import config
from . import domain
from . import run
from . import init
from . import routes

31
halfapi/cli/config.py Normal file
View File

@ -0,0 +1,31 @@
#!/usr/bin/env python3
"""
cli/config.py Contains the .halfapi/config
Defines the "halfapi config" command
"""
import click
from .cli import cli
from ..conf import CONFIG
DOMAIN_CONF_STR="""
[domain]
name = {name}
router = {router}
"""
CONF_STR="""
[project]
host = {host}
port = {port}
production = {production}
"""
@cli.command()
def config():
"""
Lists config parameters and their values
"""
click.echo(CONF_STR.format(**CONFIG))

242
halfapi/cli/domain.py Normal file
View File

@ -0,0 +1,242 @@
#!/usr/bin/env python3
"""
cli/domain.py Defines the "halfapi domain" cli commands
"""
# builtins
import os
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 ..lib.routes import api_routes
from ..lib.responses import ORJSONResponse
from ..conf import CONFIG, PROJECT_LEVEL_KEYS
from ..logging import logger
#################
# domain create #
#################
def create_domain(domain_name: str, module_path: str):
logger.info('Will add **%s** (%s) to current halfAPI project',
domain_name, module_path)
#if domain_name in DOMAINSDICT():
# logger.warning('Domain **%s** is already in project', domain_name)
# sys.exit(1)
def domain_tree_create():
def create_init(path):
with open(os.path.join(os.getcwd(), path, '__init__.py'), 'w') as f:
f.writelines([
'"""',
f'name: {domain_name}',
f'router: {module_path}',
'"""'
])
logger.debug('created %s', os.path.join(os.getcwd(), path, '__init__.py'))
def create_acl(path):
with open(os.path.join(path, 'acl.py'), 'w') as f:
f.writelines([
'from halfapi.lib.acl import public, ACLS',
])
logger.debug('created %s', os.path.join(path, 'acl.py'))
os.mkdir(domain_name)
create_init(domain_name)
router_path = os.path.join(domain_name, 'routers')
create_acl(domain_name)
os.mkdir(router_path)
create_init(router_path)
# TODO: Generate config file
domain_tree_create()
"""
try:
importlib.import_module(module_path)
except ImportError:
logger.error('cannot import %s', domain_name)
domain_tree_create()
"""
"""
try:
importlib.import_module(domain_name)
except ImportError:
click.echo('Error in domain creation')
logger.debug('%s', subprocess.run(['tree', 'a', os.getcwd()]))
raise Exception('cannot create domain {}'.format(domain_name))
"""
###############
# domain read #
###############
def list_routes(domain, m_dom):
"""
Echoes the list of the **m_dom** active routes
"""
click.echo(f'\nDomain : {domain}\n')
routes = api_routes(m_dom)[0]
if len(routes):
for key, item in routes.items():
methods = '|'.join(list(item.keys()))
click.echo(f'\t{key} : {methods}')
else:
click.echo('\t**No ROUTES**')
raise Exception('Routeless domain')
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)
@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('--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):
"""
The "halfapi domain" command
Parameters:
domain (str|None): The domain name
The parameter has a misleading name as it is a multiple option
but this would be strange to use it several times named as "domains"
update (boolean): If set, update the database for the selected domains
"""
if not domain:
if create:
# 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:
raise NotImplementedError
elif update:
raise NotImplementedError
elif delete:
raise NotImplementedError
elif read:
from ..halfapi import HalfAPI
halfapi = HalfAPI(CONFIG)
click.echo(orjson.dumps(
halfapi.domains[domain].schema(),
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)

61
halfapi/cli/init.py Normal file
View File

@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""
cli/init.py Defines the "halfapi init" cli commands
Helps the user to create a new project
"""
# builtins
import os
import sys
import re
import click
from .. import __version__
from ..conf import CONF_DIR
from .cli import cli
from ..logging import logger
TMPL_HALFAPI_ETC = """[project]
host = 127.0.0.1
port = 8000
secret = /path/to/secret_file
production = False
base_dir = {base_dir}
"""
TMPL_HALFAPI_CONFIG = """[project]
halfapi_version = {halfapi_version}
[domain]
"""
@click.argument('project')
@cli.command()
def init(project):
"""
The "halfapi init" command
"""
if not re.match('^[a-z0-9_]+$', project, re.I):
click.echo('Project name must match "^[a-z0-9_]+$", retry.', err=True)
sys.exit(1)
if os.path.exists(project):
click.echo(f'A file named {project} already exists, abort.', err=True)
sys.exit(1)
logger.debug('Create directory %s', project)
os.mkdir(project)
logger.debug('Create directory %s/.halfapi', project)
os.mkdir(f'{project}/.halfapi')
with open(f'{project}/.halfapi/config', 'w') as conf_file:
conf_file.write(TMPL_HALFAPI_CONFIG.format(
halfapi_version=__version__))
click.echo(f'Configure halfapi project in {CONF_DIR}/{project}')

54
halfapi/cli/routes.py Normal file
View File

@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""
cli/routes.py Defines the "halfapi routes" cli commands
"""
# builtins
import sys
import importlib
from pprint import pprint
import orjson
import click
from .cli import cli
from ..logging import logger
# from ..lib.domain import domain_schema_dict
from ..lib.constants import DOMAIN_SCHEMA, ROUTE_SCHEMA
from ..lib.responses import ORJSONResponse
@click.argument('module', required=True)
@click.option('--export', default=False, is_flag=True)
@click.option('--validate', default=False, is_flag=True)
@click.option('--check', default=False, is_flag=True)
@click.option('--noheader', default=False, is_flag=True)
@click.option('--schema', default=False, is_flag=True)
@cli.command()
def routes(module, export=False, validate=False, check=False, noheader=False, schema=False):
"""
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
# 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 validate:
#  routes_d = domain_schema_dict(mod)
#  try:
#  ROUTE_SCHEMA.validate(routes_d)
#  except Exception as exc:
#  raise exc

87
halfapi/cli/run.py Normal file
View File

@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""
cli/domain.py Defines the "halfapi run" cli command
"""
import os
import sys
import click
import uvicorn
from .cli import cli
from .domain import list_api_routes
from ..conf import CONFIG, SCHEMA
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('--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('--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('domain', required=False)
@cli.command()
def run(host, port, reload, secret, production, loglevel, prefix, check, dryrun,
schema, domain):
"""
The "halfapi run" command
"""
logger.debug('[run] host=%s port=%s reload=%s secret=%s production=%s loglevel=%s prefix=%s schema=%s',
host, port, reload, secret, production, loglevel, prefix, schema
)
port = int(port)
if production and reload:
reload = False
raise Exception('Can\'t use live code reload in production')
click.echo(f'Launching application')
if secret:
CONFIG['secret'] = secret
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'reload: {reload}\n'
)
if dryrun:
CONFIG['dryrun'] = True
uvicorn.run('halfapi.app:application',
host=host,
port=int(port),
log_level=loglevel,
reload=reload)

205
halfapi/conf.py Normal file
View File

@ -0,0 +1,205 @@
#!/usr/bin/env python3
"""
conf.py reads the current configuration files
It uses the following environment variables :
- HALFAPI_CONF_DIR (default: /etc/half_api)
It defines the following globals :
- PROJECT_NAME (str) - HALFAPI_PROJECT_NAME
- PRODUCTION (bool) - HALFAPI_PRODUCTION
- LOGLEVEL (str) - 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
It reads the following ressource :
- ./.halfapi/config
It follows the following format :
[project]
halfapi_version = HALFAPI_VERSION
[domain.domain_name]
name = domain_name
routers = routers
[domain.domain_name.config]
option = Argh
"""
from .logging import logger
import os
from os import environ
import sys
import importlib
import tempfile
import uuid
import toml
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'])
HALFAPI_ETC_FILE=os.path.join(
CONF_DIR, 'config'
)
BASE_DIR = os.environ.get('HALFAPI_BASE_DIR', DEFAULT_CONF['BASE_DIR'])
HALFAPI_DOT_FILE=os.path.join(
BASE_DIR, '.halfapi', 'config')
HALFAPI_CONFIG_FILES = []
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)
ENVIRONMENT = {}
# Load environment variables allowed in configuration
if 'HALFAPI_DRYRUN' in os.environ:
ENVIRONMENT['dryrun'] = True
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):
"""
The highest index in "filenames" are the highest priorty
"""
d_res = {}
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)
PROJECT_NAME = CONFIG.get('project_name',
os.environ.get('HALFAPI_PROJECT_NAME', DEFAULT_CONF['PROJECT_NAME']))
if os.environ.get('HALFAPI_DOMAIN_NAME'):
# Force enabled domain by environment variable
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]:
domain_config = CONFIG['domain'][DOMAIN_NAME]['config']
else:
domain_config = {}
CONFIG['domain'] = {}
CONFIG['domain'][DOMAIN_NAME] = {
'enabled': True,
'name': DOMAIN_NAME,
'prefix': False
}
CONFIG['domain'][DOMAIN_NAME]['config'] = domain_config
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')
# !!!

513
halfapi/half_domain.py Normal file
View File

@ -0,0 +1,513 @@
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

109
halfapi/half_route.py Normal file
View File

@ -0,0 +1,109 @@
""" HalfRoute
Child class of starlette.routing.Route
"""
from functools import partial, wraps
from typing import Callable, Coroutine, List, Dict
from types import FunctionType
from starlette.requests import Request
from starlette.responses import PlainTextResponse
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()
super().__init__(
path,
HalfRoute.acl_decorator(
fct,
params
),
methods=[method])
@staticmethod
def acl_decorator(fct: Callable = None, params: List[Dict] = None) -> Coroutine:
"""
Decorator for async functions that calls pre-conditions functions
and appends kwargs to the target function
Parameters:
fct (Callable):
The function to decorate
params List[Dict]:
A list of dicts that have an "acl" key that points to a function
Returns:
async function
"""
if not params:
params = []
if not fct:
return partial(HalfRoute.acl_decorator, params=params)
@wraps(fct)
async def caller(req: Request, *args, **kwargs):
for param in params:
if param.get('acl'):
passed = param['acl'](req, *args, **kwargs)
if isinstance(passed, FunctionType):
passed = param['acl']()(req, *args, **kwargs)
if not passed:
logger.debug(
'ACL FAIL for current route (%s - %s)', fct, param.get('acl'))
continue
logger.debug(
'ACL OK for current route (%s - %s)', fct, param.get('acl'))
req.scope['acl_pass'] = param['acl'].__name__
if 'args' in param:
req.scope['args'] = param['args']
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__)
logger.debug('acl_decorator %s', param)
logger.debug('calling %s:%s %s %s', fct.__module__, fct.__name__, args, kwargs)
return await fct(
req, *args,
**{
**kwargs,
})
if 'check' in req.query_params:
return PlainTextResponse('')
raise HTTPException(401)
return caller

316
halfapi/halfapi.py Normal file
View File

@ -0,0 +1,316 @@
#!/usr/bin/env python3
"""
app.py is the file that is read when launching the application using an asgi
runner.
It defines the following globals :
- routes (contains the Route objects for the application)
- application (the asgi application itself - a starlette object)
"""
import sys
import logging
import time
import importlib
from datetime import datetime
# asgi framework
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.requests import Request
from starlette.responses import Response, PlainTextResponse
from starlette.middleware.authentication import AuthenticationMiddleware
from timing_asgi import TimingMiddleware
from timing_asgi.integrations import StarletteScopeToName
# module libraries
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.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
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')
self.PRODUCTION = PRODUCTION
self.SECRET = SECRET
# Domains
""" HalfAPI routes (if not PRODUCTION, includes debug routes)
"""
routes = []
routes.append(
Mount('/halfapi', routes=list(self.halfapi_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
logger.info('Domain-less mode : the given schema defines the activated routes')
for route in gen_schema_routes(d_routes):
routes.append(route)
else:
pass
startup_fcts = []
if DRYRUN:
startup_fcts.append(
HalfAPI.wait_quit()
)
super().__init__(
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)
}
)
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))
if SECRET:
self.add_middleware(
AuthenticationMiddleware,
backend=JWTAuthenticationBackend(),
on_error=on_auth_error
)
if not PRODUCTION and TIMINGMIDDLEWARE:
self.add_middleware(
TimingMiddleware,
client=HTimingClient(),
metric_namer=StarletteScopeToName(prefix="halfapi",
starlette_app=self)
)
@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
def halfapi_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('/version', self.version_async)
""" Halfapi debug routes definition
"""
if self.PRODUCTION:
return
""" Debug routes
"""
async def debug_log(request: Request, *args, **kwargs):
logger.debug('debuglog# %s', {datetime.now().isoformat()})
logger.info('debuglog# %s', {datetime.now().isoformat()})
logger.warning('debuglog# %s', {datetime.now().isoformat()})
logger.error('debuglog# %s', {datetime.now().isoformat()})
logger.critical('debuglog# %s', {datetime.now().isoformat()})
return Response('')
yield Route('/log', debug_log)
async def error_code(request: Request, *args, **kwargs):
code = request.path_params['code']
raise HTTPException(code)
yield Route('/error/{code:int}', error_code)
async def exception(request: Request, *args, **kwargs):
raise Exception('Test exception')
yield Route('/exception', exception)
@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__()

178
halfapi/lib/acl.py Normal file
View File

@ -0,0 +1,178 @@
#!/usr/bin/env python3
"""
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
def public(*args, **kwargs) -> bool:
"Unlimited access"
return True
def private(*args, **kwargs) -> bool:
"Forbidden access"
return False
def connected(fct=public):
""" Decorator that checks if the user object of the request has been set
"""
@wraps(fct)
def caller(req, *args, **kwargs):
if (not hasattr(req, 'user')
or isinstance(req.user, UnauthenticatedUser)
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
def args_check(fct):
""" Decorator that puts required and optional arguments in scope
For GET requests it uses the query_params
For POST requests it uses the body as JSON
If "check" is present in the query params, nothing is done.
If some required arguments are missing, a 400 status code is sent.
"""
@wraps(fct)
async def caller(req, *args, **kwargs):
if 'check' in req.query_params:
# Check query param should not read the "args"
return await fct(req, *args, **kwargs)
data_ = {}
if req.method == 'GET':
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 ''
def comma_list(array: list) -> str:
return ', '.join(array)
args_d = req.scope.get('args')
if args_d is not None and isinstance(data_, dict):
required = args_d.get('required', set())
missing = []
data = {}
for key in required:
data[key] = data_.get(key, None)
if data[key] is None:
missing.append(key)
if missing:
raise HTTPException(
400,
f"Missing value{plural(missing)} for: {comma_list(missing)}!")
optional = args_d.get('optional', set())
for key in optional:
if key in data_:
data[key] = data_[key]
else:
""" Unsafe mode, without specified arguments, or plain text mode
"""
data = data_
kwargs['data'] = data
out_s = req.scope.get('out')
if out_s:
kwargs['out'] = list(out_s)
return await fct(req, *args, **kwargs)
return caller
# 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)
)
@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)

View File

@ -1,158 +0,0 @@
#!/usr/bin/env python3
from os import environ
from starlette.exceptions import HTTPException
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.routing import Match, Mount
from starlette.types import ASGIApp, Receive, Scope, Send
from halfapi.models.api.view.acl import Acl as AclView
class DebugRouteException(Exception):
def __init__(self, *args, **kwargs):
super().__init__(self)
def match_route(app: ASGIApp, scope: Scope):
""" Checks all routes from "app" and checks if it matches with the one from
scope
Parameters:
- app (ASGIApp): The Starlette instance
- scope (MutableMapping[str, Any]): The requests scope
Returns:
- (dict, dict): The first dict of the tuple is the details on the
route, the second one is the path parameters
Raises:
HTTPException
"""
""" The *result* variable is fitted to the filter that will be applied when
searching the route in the database.
Refer to the database documentation for more details on the api.route
table.
"""
print('3')
from ..app import CONFIG
print(CONFIG)
result = {
'domain': None,
'name': None,
'http_verb': None,
'version': None
}
if 'DEBUG' in CONFIG.keys() and len(scope['path'].split('/')) <= 3:
raise DebugRouteException()
try:
""" Identification of the parts of the path
Examples :
version : v4
domain : organigramme
path : laboratoire/personnel
"""
_, result['domain'], path = scope['path'].split('/', 2)
except ValueError as e:
#404 Not found
raise HTTPException(404)
# Prefix the path with "/"
path = f'/{path}'
for route in app.routes:
if type(route) != Mount:
""" The root app should not have exposed routes,
only the mounted domains have some.
"""
continue
""" Clone the scope to assign the path to the path without the
matching domain, be careful to the "root_path" of the mounted domain.
@TODO
Also, improper array unpacking may make crash the program without any
explicit error, we may have to improve this as we only rely on this
function to accomplish all the routing
"""
subscope = scope.copy()
_, result['domain'], subpath = path.split('/', 2)
subscope['path'] = f'/{subpath}'
for mount_route in route.routes:
# Parse all domain routes
submatch = mount_route.matches(subscope)
if submatch[0] != Match.FULL:
continue
# Route matches
try:
result['name'] = submatch[1]['endpoint'].__name__
result['http_verb'] = scope['method']
except Exception as e:
print(e)
return result, submatch[1]['path_params']
raise HTTPException(404)
class AclCallerMiddleware(BaseHTTPMiddleware):
async def __call__(self, scope:Scope, receive: Receive, send: Send) -> None:
""" Points out to the domain which ACL function it should call
Parameters :
- request (Request): The current request
- call_next (RequestResponseEndpoint): The next middleware/route function
Return:
Response
"""
print('Hit AclCallerMiddleware of API')
if scope['type'] != 'http':
await self.app(scope, receive, send)
return
app = self.app
while True:
if not hasattr(app, 'app'):
break
app = app.app
if scope['path'].split('/')[-1] not in ['docs','openapi.json','redoc']:
# routes in the the database, the others being
# docs/openapi.json/redoc
try:
d_match, path_params = match_route(app, scope)
scope['acls'] = []
for acl in AclView(**d_match).select():
# retrieve related ACLs
if ('acl_function_name' not in acl.keys()
or 'domain' not in acl.keys()):
continue
scope['acls'].append(acl['acl_function_name'])
except StopIteration:
# TODO : No ACL sur une route existante, prevenir l'admin?
print("No ACL")
pass
except DebugRouteException:
print("Debug route")
if 'DEBUG_ACL' in environ.keys():
scope['acls'] = environ['DEBUG_ACL'].split(':')
else:
scope['acls'] = []
return await self.app(scope, receive, send)

View File

@ -1,35 +0,0 @@
#!/usr/bin/env python3
from starlette.requests import Request
from starlette.exceptions import HTTPException
from starlette.middleware.base import BaseHTTPMiddleware
class AclMiddleware(BaseHTTPMiddleware):
def __init__(self, app, acl_module):
super().__init__(app)
self.acl_module = acl_module
async def dispatch(self, request: Request, call_next):
""" Checks the "acls" key in the scope and applies the
corresponding functions in the current module's acl lib.
Raises an exception if no acl function returns True
"""
print(f'Hit acl {__name__} middleware')
for acl_fct_name in request.scope['acls']:
print(f'Will apply {acl_fct_name}')
try:
fct = getattr(self.acl_module, acl_fct_name)
if fct(request) is True:
return await call_next(request)
except AttributeError as e:
print(f'No ACL function "{acl_fct_name}" in {__name__} module')
print(e)
break
except Exception as e:
print(e)
raise HTTPException(500)
raise HTTPException(401)

71
halfapi/lib/constants.py Normal file
View File

@ -0,0 +1,71 @@
import re
from schema import Schema, Optional, Or
from .. import __version__
VERBS = ('GET', 'POST', 'PUT', 'PATCH', 'DELETE')
ITERABLE_STR = Or([ str ], { str }, ( str ))
ACLS_SCHEMA = Schema([{
'acl': str,
Optional('args'): {
Optional('required'): ITERABLE_STR,
Optional('optional'): ITERABLE_STR
},
Optional('out'): ITERABLE_STR
}])
ROUTER_ACLS_SCHEMA = Schema([{
'acl': lambda n: callable(n),
Optional('args'): {
Optional('required'): ITERABLE_STR,
Optional('optional'): ITERABLE_STR
},
Optional('out'): ITERABLE_STR
}])
is_callable_dotted_notation = lambda x: re.match(
r'^(([a-zA-Z_])+\.?)*:[a-zA-Z_]+$', 'ab_c.TEST:get')
ROUTE_SCHEMA = Schema({
Optional(str): { # path - Optional when no routes
str: { # method
'callable': is_callable_dotted_notation,
'docs': lambda n: True, # Should validate an openAPI spec
'acls': ACLS_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)]
]
})
API_SCHEMA_DICT = {
'openapi': '3.0.0',
'info': {
'title': 'HalfAPI',
'version': __version__
},
}
API_SCHEMA = Schema({
**API_SCHEMA_DICT,
'domain': DOMAIN_SCHEMA,
'paths': ROUTE_SCHEMA
})
ROUTER_SCHEMA = Schema({
Or('', str): {
# Optional('GET'): [],#ACLS_SCHEMA,
Optional(Or(*VERBS)): ROUTER_ACLS_SCHEMA,
Optional('SUBROUTES'): [Optional(str)]
}
})

192
halfapi/lib/domain.py Normal file
View File

@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""
lib/domain.py The domain-scoped utility functions
"""
import re
import sys
import importlib
import inspect
from functools import wraps
from types import ModuleType, FunctionType
from typing import Coroutine, Generator
from typing import Dict, List, Tuple
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.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):
""" The exception that is raised when HalfAPI is called without domains
"""
pass
def route_decorator(fct: FunctionType) -> Coroutine:
""" Returns an async function that can be mounted on a router
"""
@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
}
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'].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
return wrapped
def get_fct_name(http_verb: str, path: str) -> str:
"""
Returns the predictable name of the function for a route
Parameters:
- http_verb (str): The Route's HTTP method (GET, POST, ...)
- path (str): The functions path
Returns:
str: The *unique* function name for a route and it's verb
Examples:
>>> get_fct_name('get', '')
'get'
>>> get_fct_name('GET', '')
'get'
>>> get_fct_name('POST', 'foo')
'post_foo'
>>> get_fct_name('POST', 'bar')
'post_bar'
>>> get_fct_name('DEL', 'foo/{boo}')
'del_foo_BOO'
>>> get_fct_name('DEL', '{boo:zoo}/far')
'del_BOO_far'
"""
if path and path[0] == '/':
path = path[1:]
fct_name = [http_verb.lower()]
for elt in path.split('/'):
if elt and elt[0] == '{':
fct_name.append(elt[1:-1].split(':')[0].upper())
elif elt:
fct_name.append(elt)
return '_'.join(fct_name)

View File

@ -0,0 +1,80 @@
"""
DomainMiddleware
"""
from starlette.datastructures import URL
from starlette.middleware.base import (BaseHTTPMiddleware,
RequestResponseEndpoint)
from starlette.requests import Request
from starlette.responses import Response
from ..logging import logger
class DomainMiddleware(BaseHTTPMiddleware):
"""
DomainMiddleware adds the api routes and acls to the following scope keys :
- api
- acl
"""
def __init__(self, app, domain=None):
""" app: HalfAPI instance
"""
logger.info('DomainMiddleware app:%s domain:%s', app, domain)
super().__init__(app)
self.domain = domain
self.request = None
async def dispatch(self, request: Request,
call_next: RequestResponseEndpoint) -> Response:
"""
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))
response = await call_next(request)
if 'acl_pass' in request.scope:
# Set the http header "x-acl" if an acl was used on the route
response.headers['x-acl'] = request.scope['acl_pass']
if 'args' in request.scope:
# Set the http headers "x-args-required" and "x-args-optional"
if len(request.scope['args'].get('required', set())):
response.headers['x-args-required'] = \
','.join(request.scope['args']['required'])
if len(request.scope['args'].get('optional', set())):
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']
return response

View File

@ -1,110 +1,162 @@
__LICENSE__ = """ """
BSD 3-Clause License JWT Middleware module
Copyright (c) 2018, Amit Ripshtos Classes:
All rights reserved. - JWTUser : goes in request.user
- JWTAuthenticationBackend
- JWTWebSocketAuthenticationBackend
Redistribution and use in source and binary forms, with or without Raises:
modification, are permitted provided that the following conditions are met: Exception: If configuration has no SECRET
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
""" """
import jwt from os import environ
import typing
from uuid import UUID from uuid import UUID
from http.cookies import SimpleCookie
import jwt
from starlette.authentication import ( from starlette.authentication import (
AuthenticationBackend, AuthenticationError, BaseUser, AuthCredentials, AuthenticationBackend, AuthenticationError, BaseUser, AuthCredentials,
UnauthenticatedUser) UnauthenticatedUser)
from starlette.requests import HTTPConnection, Request
from starlette.exceptions import HTTPException
from .user import CheckUser, JWTUser, Nobody
from ..logging import logger
from ..conf import CONFIG
from ..lib.responses import ORJSONResponse
class JWTUser(BaseUser): SECRET=None
def __init__(self, id: UUID, token: str, payload: dict) -> None:
self.__id = id
self.token = token
self.payload = payload
def __str__(self): try:
return str({ with open(CONFIG.get('secret', ''), 'r') as secret_file:
'id' : str(self.__id), SECRET = secret_file.read().strip()
'token': self.token, except FileNotFoundError:
'payload': self.payload logger.error('Could not import SECRET variable from conf module,'\
}) ' using HALFAPI_SECRET environment variable')
@property
def is_authenticated(self) -> bool: def cookies_from_scope(scope):
return True 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,
algorithm: str = 'HS256', prefix: str = 'JWT'):
if secret_key is None:
raise Exception('Missing secret_key argument for JWTAuthenticationBackend')
self.secret_key = secret_key
self.algorithm = algorithm
self.prefix = prefix
@property @property
def id(self) -> str: def id(self) -> str:
return self.__id return self.__id
async def authenticate(
self, conn: HTTPConnection
) -> typing.Optional[typing.Tuple['AuthCredentials', 'BaseUser']]:
class JWTAuthenticationBackend(AuthenticationBackend): # Standard way to authenticate via API
def __init__(self, secret_key: str, algorithm: str = 'HS256', prefix: str = 'JWT', name: str = 'name'): # https://datatracker.ietf.org/doc/html/rfc7235#section-4.2
self.secret_key = secret_key token = conn.headers.get('Authorization')
self.algorithm = algorithm
self.prefix = prefix
self.id = id
async def authenticate(self, request): if not token:
if "Authorization" not in request.headers: token = cookies_from_scope(conn.scope).get('Authorization')
return None
is_check_call = 'check' in conn.query_params
PRODUCTION = conn.scope['app'].debug == False
if not token and not is_check_call:
return AuthCredentials(), Nobody()
token = request.headers["Authorization"]
try: try:
payload = jwt.decode(token, key=self.secret_key, algorithms=self.algorithm) if token:
except jwt.InvalidTokenError as e: payload = jwt.decode(token,
raise AuthenticationError(str(e)) key=self.secret_key,
except Exception as e: algorithms=[self.algorithm],
print(e) options={
'verify_signature': True
})
if is_check_call:
if token:
return AuthCredentials(), CheckUser(payload['user_id'])
return AuthCredentials(), Nobody()
if PRODUCTION and 'debug' in payload.keys() and payload['debug']:
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:
logger.error('Authentication error : %s', exc)
raise exc
return AuthCredentials(["authenticated"]), JWTUser( return AuthCredentials(["authenticated"]), JWTUser(
id=payload['id'], token=token, payload=payload) user_id=payload['user_id'], token=token, payload=payload)
class JWTWebSocketAuthenticationBackend(AuthenticationBackend): class JWTWebSocketAuthenticationBackend(AuthenticationBackend):
def __init__(self, secret_key: str, algorithm: str = 'HS256', query_param_name: str = 'jwt', def __init__(self, secret_key: str, algorithm: str = 'HS256', query_param_name: str = 'jwt',
id: UUID = None, audience = None, options = {}): user_id: UUID = None, audience = None):
self.secret_key = secret_key self.secret_key = secret_key
self.algorithm = algorithm self.algorithm = algorithm
self.query_param_name = query_param_name self.query_param_name = query_param_name
self.id = id self.__id = user_id
self.audience = audience self.audience = audience
self.options = options
async def authenticate(self, request): async def authenticate(
if self.query_param_name not in request.query_params: self, conn: HTTPConnection
return AuthCredentials(), UnauthenticatedUser() ) -> typing.Optional[typing.Tuple["AuthCredentials", "BaseUser"]]:
token = request.query_params[self.query_param_name] if self.query_param_name not in conn.query_params:
return AuthCredentials(), Nobody()
token = conn.query_params[self.query_param_name]
try: try:
payload = jwt.decode(token, key=self.secret_key, algorithms=self.algorithm, payload = jwt.decode(
audience=self.audience, options=self.options) token,
except jwt.InvalidTokenError as e: key=self.secret_key,
raise AuthenticationError(str(e)) algorithms=[self.algorithm],
audience=self.audience,
options={
'verify_signature': bool(PRODUCTION)
})
return AuthCredentials(["authenticated"]), JWTUser(id = payload['id'], if PRODUCTION and 'debug' in payload.keys() and payload['debug']:
token=token, payload=payload) raise AuthenticationError(
'Trying to connect using *DEBUG* token in *PRODUCTION* mode')
except jwt.InvalidTokenError as exc:
raise AuthenticationError(str(exc)) from exc
return (
AuthCredentials(["authenticated"]),
JWTUser(
user_id=payload['id'],
token=token,
payload=payload)
)

View File

@ -1,22 +1,25 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from starlette.exceptions import HTTPException
from .responses import CSVResponse
""" """
This is the *query* library that contains all the useful functions to treat our This is the *query* library that contains all the useful functions to treat our
queries queries
Fonction:
- parse_query
""" """
def parse_query(q: str = ""): from starlette.exceptions import HTTPException
def parse_query(q_string: str = ""):
""" """
Returns the fitting Response object according to query parameters. Returns the fitting Response object according to query parameters.
The parse_query function handles the following arguments in the query The parse_query function handles the following arguments in the query
string : format, limit, and offset string : format, limit, and offset
It returns a callable function that returns the desired Response object. It returns a callable function that returns the desired Response object.
Parameters: Parameters:
q (str): The query string "q" parameter, in the format q_string (str): The query string "q" parameter, in the format
key0:value0|...|keyN:valueN key0:value0|...|keyN:valueN
Returns: Returns:
@ -53,25 +56,23 @@ def parse_query(q: str = ""):
>>> parse_query('limit:10') >>> parse_query('limit:10')
<function parse_query.<locals>.select at 0x...> <function parse_query.<locals>.select at 0x...>
>>> parse_query('limit=10') # >>> parse_query('limit=10')
Traceback (most recent call last): # starlette.exceptions.HTTPException: 400
...
starlette.exceptions.HTTPException: 400
""" """
params = {} params = {}
if len(q) > 0: if len(q_string) > 0:
try: try:
split_ = lambda x : x.split(':') split_ = lambda x : x.split(':')
params = dict(map(split_, q.split('|'))) params = dict(map(split_, q_string.split('|')))
except ValueError: except ValueError as exc:
raise HTTPException(400) raise HTTPException(400) from exc
split_ = lambda x : x.split(':') split_ = lambda x : x.split(':')
params = dict(map(split_, q.split('|'))) params = dict(map(split_, q_string.split('|')))
def select(obj): def select(obj, fields):
if 'limit' in params and int(params['limit']) > 0: if 'limit' in params and int(params['limit']) > 0:
obj.limit(int(params['limit'])) obj.limit(int(params['limit']))
@ -79,9 +80,6 @@ def parse_query(q: str = ""):
if 'offset' in params and int(params['offset']) > 0: if 'offset' in params and int(params['offset']) > 0:
obj.offset(int(params['offset'])) obj.offset(int(params['offset']))
if 'format' in params and params['format'] == 'csv': return list(obj.select(*fields))
return CSVResponse([elt for elt in obj.select()])
return [elt for elt in obj.select()]
return select return select

View File

@ -1,59 +1,167 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# builtins # builtins
import csv """ Response module
Contains some base response classes
Classes :
- HJSONResponse
- InternalServerErrorResponse
- NotFoundResponse
- NotImplementedResponse
- ORJSONResponse
- PlainTextResponse
- ServiceUnavailableResponse
- UnauthorizedResponse
- ODSResponse
"""
from datetime import date from datetime import date
from io import TextIOBase, StringIO import decimal
import typing
from io import BytesIO
import orjson
# asgi framework # asgi framework
from starlette.responses import PlainTextResponse, Response from starlette.responses import PlainTextResponse, Response, JSONResponse, HTMLResponse
from starlette.requests import Request
from starlette.exceptions import HTTPException
__all__ = ['CSVResponse', from .user import JWTUser, Nobody
from ..logging import logger
__all__ = [
'HJSONResponse',
'InternalServerErrorResponse', 'InternalServerErrorResponse',
'NotFoundResponse', 'NotFoundResponse',
'NotImplementedResponse', 'NotImplementedResponse',
'ORJSONResponse',
'PlainTextResponse', 'PlainTextResponse',
'ServiceUnavailableResponse',
'UnauthorizedResponse'] 'UnauthorizedResponse']
class CSVResponse(Response):
def __init__(self, obj):
with StringIO() as csv_file:
csv_obj = csv.writer(csv_file, dialect="excel")
csv_obj.writerows([elt.values() for elt in obj])
filename = f'Personnels_LIRMM-{date.today()}.csv'
super().__init__(
content=csv_file.getvalue(),
headers={
'Content-Type': 'text/csv; charset=UTF-8',
'Content-Disposition': f'attachment; filename="{filename}"'},
status_code = 200)
class InternalServerErrorResponse(Response): class InternalServerErrorResponse(Response):
""" The 500 Internal Server Error default Response """ The 500 Internal Server Error default Response
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(status_code=500) super().__init__(status_code=500)
class NotFoundResponse(Response): class NotFoundResponse(Response):
""" The 404 Not Found default Response """ The 404 Not Found default Response
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(status_code=404) super().__init__(status_code=404)
class NotImplementedResponse(Response): class NotImplementedResponse(Response):
""" The 501 Not Implemented default Response """ The 501 Not Implemented default Response
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(status_code=501) super().__init__(status_code=501)
class ServiceUnavailableResponse(Response):
""" The 503 Service Unavailable default Response
"""
def __init__(self, *args, **kwargs):
super().__init__(status_code=503)
class UnauthorizedResponse(Response): class UnauthorizedResponse(Response):
""" The 401 Not Found default Response """ The 401 Not Found default Response
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(status_code = 401) super().__init__(status_code = 401)
class ORJSONResponse(JSONResponse):
""" The response that encodes data into JSON
"""
def __init__(self, content, default=None, **kwargs):
self.default = default if default is not None else ORJSONResponse.default_cast
super().__init__(content, **kwargs)
def render(self, content: typing.Any) -> bytes:
return orjson.dumps(content,
option=orjson.OPT_NON_STR_KEYS,
default=self.default)
@staticmethod
def default_cast(typ):
""" Cast the data in JSON-serializable type
"""
str_types = {
decimal.Decimal
}
list_types = {
set
}
jsonable_types = {
JWTUser, Nobody
}
if callable(typ):
return typ.__name__
if type(typ) in str_types:
return str(typ)
if type(typ) in list_types:
return list(typ)
if type(typ) in jsonable_types:
return typ.json
raise TypeError(f'Type {type(typ)} is not handled by ORJSONResponse')
class HJSONResponse(ORJSONResponse):
""" The response that encodes generator data into JSON
"""
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

12
halfapi/lib/router.py Normal file
View File

@ -0,0 +1,12 @@
import os
import sys
import subprocess
from types import ModuleType
from typing import Dict
from pprint import pprint
from schema import SchemaError
from .constants import VERBS, ROUTER_SCHEMA
from ..logging import logger

136
halfapi/lib/routes.py Normal file
View File

@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""
Routes module
Classes :
- JSONRoute
Fonctions :
- gen_domain_routes
- gen_schema_routes
- api_routes
Exception :
- DomainNotFoundError
"""
import inspect
from typing import Coroutine, Dict, Generator, Tuple, Any
from types import ModuleType, FunctionType
import yaml
# from .domain import gen_router_routes, domain_acls, route_decorator, domain_schema
from .responses import ORJSONResponse
from .acl import args_check
from ..half_route import HalfRoute
from . import acl
from ..logging import logger
class DomainNotFoundError(Exception):
""" Exception when a domain is not importable
"""
def JSONRoute(data: Any) -> Coroutine:
"""
Returns a route function that returns the data as JSON
Parameters:
data (Any):
The data to return
Returns:
async function
"""
async def wrapped(request, *args, **kwargs):
return ORJSONResponse(data)
return wrapped
def gen_domain_routes(m_domain: ModuleType):
"""
Yields the Route objects for a domain
Parameters:
m_domains: 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)
def gen_schema_routes(schema: Dict):
"""
Yields the Route objects according to a given schema
"""
for path, methods in schema.items():
for verb, definition in methods.items():
fct = definition.pop('fct')
acls = definition.pop('acls')
# TODO: Check what to do with gen_routes, it is almost the same function
if not inspect.iscoroutinefunction(fct):
yield HalfRoute(path, route_decorator(fct), acls, verb)
else:
yield HalfRoute(path, args_check(fct), acls, verb)
def api_routes(m_dom: ModuleType) -> Tuple[Dict, Dict]:
"""
Yields the description objects for HalfAPI app routes
Parameters:
m_dom (ModuleType): the halfapi module
Returns:
(Dict, Dict)
"""
d_acls = {}
def str_acl(params):
l_params = []
for param in params:
if 'acl' not in param.keys() or not param['acl']:
continue
l_params.append(param.copy())
l_params[-1]['acl'] = param['acl'].__name__
if param['acl'] not in d_acls.keys():
d_acls[param['acl'].__name__] = param['acl']
return l_params
d_res = {}
for path, verb, m_router, fct, params in gen_router_routes(m_dom, []):
try:
if path not in d_res:
d_res[path] = {}
d_res[path][verb] = {
'docs': yaml.load(fct.__doc__, Loader=yaml.FullLoader),
'acls': str_acl(params)
}
except Exception as exc:
logger.error("""Error in route generation
path:%s
verb:%s
router:%s
fct:%s
params:%s """, path, verb, m_router, fct, params)
raise exc
return d_res, d_acls

137
halfapi/lib/schemas.py Normal file
View File

@ -0,0 +1,137 @@
""" Schemas module
Functions :
- schema_json
- schema_dict_dom
- get_acls
Constant :
SCHEMAS (starlette.schemas.SchemaGenerator)
"""
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 ..logging import logger
from .routes import api_routes
from .responses import ORJSONResponse
SCHEMAS = SchemaGenerator(
{"openapi": "3.0.0", "info": {"title": "HalfAPI", "version": __version__}}
)
async def schema_json(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(
SCHEMAS.get_schema(routes=request.app.routes))
def schema_csv_dict(csv: List[str], prefix='/') -> Dict:
package = None
schema_d = {}
modules_d = {}
acl_modules_d = {}
for line in csv:
if not line:
continue
path, verb, router, acl_fct_name, args_req, args_opt, out = line.strip().split(';')
logger.info('schema_csv_dict %s %s %s', path, args_req, args_opt)
path = f'{prefix}{path}'
if path not in schema_d:
schema_d[path] = {}
if verb not in schema_d[path]:
mod_str = router.split(':')[0]
fct_str = router.split(':')[1]
if mod_str not in modules_d:
modules_d[mod_str] = importlib.import_module(mod_str)
if not hasattr(modules_d[mod_str], fct_str):
raise Exception(
'Missing function in module. module:{} function:{}'.format(
router, fct_str
)
)
fct = getattr(modules_d[mod_str], fct_str)
schema_d[path][verb] = {
'module': modules_d[mod_str],
'fct': fct,
'acls': []
}
if package and router.split('.')[0] != package:
raise Exception('Multi-domain is not allowed in that mode')
package = router.split('.')[0]
if not len(package):
raise Exception(
'Empty package name (router=%s)'.format(router))
acl_package = f'{package}.acl'
if acl_package not in acl_modules_d:
if acl_package not in modules_d:
modules_d[acl_package] = importlib.import_module(acl_package)
if not hasattr(modules_d[acl_package], acl_fct_name):
raise Exception(
'Missing acl function in module. module:{} acl:{}'.format(
acl_package, acl_fct_name
)
)
acl_modules_d[acl_package] = {}
acl_modules_d[acl_package][acl_fct_name] = getattr(modules_d[acl_package], acl_fct_name)
schema_d[path][verb]['acls'].append({
'acl': acl_modules_d[acl_package][acl_fct_name],
'args': {
'required': set(args_req.split(',')) if len(args_req) else set(),
'optional': set(args_opt.split(',')) if len(args_opt) else set()
}
})
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]
}
})

23
halfapi/lib/timing.py Normal file
View File

@ -0,0 +1,23 @@
"""
Timing module
Helpers to gathers stats on requests timing
class HTimingClient
"""
import logging
from timing_asgi import TimingClient
from ..logging import logger
class HTimingClient(TimingClient):
""" Used to redefine TimingClient.timing
"""
def timing(self, metric_name, timing, tags):
tags_d = dict(map(lambda elt: elt.split(':'), tags))
logger.debug('[TIME:%s][%s] %s %s - %sms',
tags_d['time'], metric_name,
tags_d['http_method'], tags_d['http_status'],
round(timing*1000, 2))

79
halfapi/lib/user.py Normal file
View File

@ -0,0 +1,79 @@
from uuid import UUID
from starlette.authentication import BaseUser, UnauthenticatedUser
class Nobody(UnauthenticatedUser):
""" Nobody class
The default class when no token is passed
"""
@property
def json(self):
return {
'id' : '',
'token': '',
'payload': ''
}
class JWTUser(BaseUser):
""" JWTUser class
Is used to store authentication informations
"""
def __init__(self, user_id: UUID, token: str, payload: dict) -> None:
self.__id = user_id
self.token = token
self.payload = payload
def __str__(self):
return str(self.json)
@property
def json(self):
return {
'id' : str(self.__id),
'token': self.token,
'payload': self.payload
}
@property
def is_authenticated(self) -> bool:
return True
@property
def display_name(self) -> str:
return ' '.join(
(self.payload.get('name'), self.payload.get('firstname')))
@property
def id(self) -> str:
return self.__id
class CheckUser(BaseUser):
""" CheckUser class
Is used to call checks with give user_id, to know if it passes the ACLs for
the given route.
It should never be able to run a route function.
"""
def __init__(self, user_id: UUID) -> None:
self.__id = user_id
@property
def is_authenticated(self) -> bool:
return True
@property
def display_name(self) -> str:
return 'check_user'
@property
def id(self) -> str:
return self.__id

32
halfapi/logging.py Normal file
View File

@ -0,0 +1,32 @@
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):
# When run by 'uvicorn ...', a root handler is already
# configured and the basicConfig below does nothing.
# To get the desired formatting:
logging.getLogger().handlers.clear()
# 'uvicorn --log-config' is broken so we configure in the app.
# https://github.com/encode/uvicorn/issues/511
logging.basicConfig(
# match gunicorn format
format=format,
datefmt=datefmt,
level=level)
# When run by 'gunicorn -k uvicorn.workers.UvicornWorker ...',
# These loggers are already configured and propogating.
# So we have double logging with a root logger.
# (And setting propagate = False hurts the other usage.)
logging.getLogger('uvicorn.asgi').handlers.clear()
logging.getLogger('uvicorn.access').handlers.clear()
logging.getLogger('uvicorn.error').handlers.clear()
logging.getLogger('uvicorn.asgi').propagate = True
logging.getLogger('uvicorn.access').propagate = True
logging.getLogger('uvicorn.error').propagate = True
logger = logging.getLogger()

View File

@ -1,11 +0,0 @@
"""This file is part of the apidb package. It has been generated by the
command halfORM. To keep it in sync with your database structure, just rerun
halfORM.
More information on the half_orm library on https://github.com/collorg/halfORM.
"""
__all__ = [
'api',
'db_connector'
]

View File

@ -1,73 +0,0 @@
create schema api;
create type verb as enum ('POST', 'GET', 'PUT', 'DELETE');
create table api.version (
name text primary key,
server cidr not null default '127.0.0.1',
port integer not null
);
create table api.domain (
version text references api.version(name),
name text,
primary key (version, name)
);
create table api.route (
path text, -- relative to /api/<version>/<domain>
version text,
domain text,
primary key (path, domain, version)
);
alter table api.route add constraint route_domain_fkey foreign key (version, domain) references api.domain(version, name) on update cascade on delete cascade;
create table api.acl_function (
name text,
description text,
version text,
domain text,
primary key (name, version, domain)
);
alter table api.acl_function add constraint acl_function_domain_fkey foreign key (version, domain) references api.domain(version, name) on update cascade on delete cascade;
create table api.acl (
name text,
http_verb verb,
path text not null,
version text,
domain text not null,
function text not null,
primary key (name, version, domain, function)
);
alter table api.acl add constraint acl_route_fkey foreign key (path, version, domain) references api.route(path, version, domain) on update cascade on delete cascade;
alter table api.acl add constraint acl_function_fkey foreign key (function, version, domain) references api.acl_function(name, version, domain) on update cascade on delete cascade;
create schema "api.view";
create view "api.view".route as
select
route.*,
version.name,
version.server,
version.port,
'/'::text || route.domain || route.path AS abs_path
from
api.route
join api.domain on
route.domain = domain.name
join api.version on
domain.version = version.name;
create view "api.view".acl as
select
acl.*,
acl_function.name as acl_function_name,
'/'::text || acl.domain || acl.path AS abs_path
from
api.acl
join api.acl_function on
acl.function = acl_function.name;

View File

@ -1,15 +0,0 @@
"""This file is part of the apidb package. It has been generated by the
command halfORM. To keep it in sync with your database structure, just rerun
halfORM.
More information on the half_orm library on https://github.com/collorg/halfORM.
"""
__all__ = [
'acl',
'acl_function',
'domain',
'route',
'version',
'view'
]

View File

@ -1,52 +0,0 @@
#-*- coding: utf-8 -*-
# pylint: disable=wrong-import-order
"""The module apidb.api.acl povides the Acl class.
WARNING!
This file is part of the apidb package. It has been generated by the
command halfORM. To keep it in sync with your database structure, just rerun
halfORM.
More information on the half_orm library on https://github.com/collorg/halfORM.
DO NOT REMOVE OR MODIFY THE LINES BEGINING WITH:
#>>> PLACE YOUR CODE BELOW...
#<<< PLACE YOUR CODE ABOVE...
MAKE SURE YOU PLACE YOUR CODE BETWEEN THESE LINES OR AT THE END OF THE FILE.
halfORM ONLY PRESERVES THE CODE BETWEEN THESE MARKS WHEN IT IS RUN.
"""
from ..db_connector import base_relation_class
#>>> PLACE YOUR CODE BELLOW THIS LINE. DO NOT REMOVE THIS LINE!
#<<< PLACE YOUR CODE ABOVE THIS LINE. DO NOT REMOVE THIS LINE!
__RCLS = base_relation_class('api.acl')
class Acl( __RCLS):
"""
__RCLS: <class 'half_orm.relation.Table_ApiApiAcl'>
This class allows you to manipulate the data in the PG relation:
TABLE: "api"."api"."acl"
FIELDS:
- name: (text) PK
- http_verb: (verb)
- path: (text) NOT NULL
- version: (text) PK
- domain: (text) PK
- function: (text) PK
FOREIGN KEYS:
- acl_route_fkey: (path, version, domain)
"api"."api"."route"(path, version, domain)
- acl_function_fkey: (function, version, domain)
"api"."api"."acl_function"(name, version, domain)
"""
def __init__(self, **kwargs):
super(Acl, self).__init__(**kwargs)
#>>> PLACE YOUR CODE BELLOW THIS LINE. DO NOT REMOVE THIS LINE!

View File

@ -1,50 +0,0 @@
#-*- coding: utf-8 -*-
# pylint: disable=wrong-import-order
"""The module apidb.api.acl_function povides the AclFunction class.
WARNING!
This file is part of the apidb package. It has been generated by the
command halfORM. To keep it in sync with your database structure, just rerun
halfORM.
More information on the half_orm library on https://github.com/collorg/halfORM.
DO NOT REMOVE OR MODIFY THE LINES BEGINING WITH:
#>>> PLACE YOUR CODE BELOW...
#<<< PLACE YOUR CODE ABOVE...
MAKE SURE YOU PLACE YOUR CODE BETWEEN THESE LINES OR AT THE END OF THE FILE.
halfORM ONLY PRESERVES THE CODE BETWEEN THESE MARKS WHEN IT IS RUN.
"""
from ..db_connector import base_relation_class
#>>> PLACE YOUR CODE BELLOW THIS LINE. DO NOT REMOVE THIS LINE!
#<<< PLACE YOUR CODE ABOVE THIS LINE. DO NOT REMOVE THIS LINE!
__RCLS = base_relation_class('api.acl_function')
class AclFunction( __RCLS):
"""
__RCLS: <class 'half_orm.relation.Table_ApiApiAcl_function'>
This class allows you to manipulate the data in the PG relation:
TABLE: "api"."api"."acl_function"
FIELDS:
- name: (text) PK
- description: (text)
- version: (text) PK
- domain: (text) PK
FOREIGN KEYS:
- _reverse_fkey_api_api_acl_function_version_domain: (name, version, domain)
"api"."api"."acl"(function, version, domain)
- acl_function_domain_fkey: (version, domain)
"api"."api"."domain"(version, name)
"""
def __init__(self, **kwargs):
super(AclFunction, self).__init__(**kwargs)
#>>> PLACE YOUR CODE BELLOW THIS LINE. DO NOT REMOVE THIS LINE!

View File

@ -1,50 +0,0 @@
#-*- coding: utf-8 -*-
# pylint: disable=wrong-import-order
"""The module apidb.api.domain povides the Domain class.
WARNING!
This file is part of the apidb package. It has been generated by the
command halfORM. To keep it in sync with your database structure, just rerun
halfORM.
More information on the half_orm library on https://github.com/collorg/halfORM.
DO NOT REMOVE OR MODIFY THE LINES BEGINING WITH:
#>>> PLACE YOUR CODE BELOW...
#<<< PLACE YOUR CODE ABOVE...
MAKE SURE YOU PLACE YOUR CODE BETWEEN THESE LINES OR AT THE END OF THE FILE.
halfORM ONLY PRESERVES THE CODE BETWEEN THESE MARKS WHEN IT IS RUN.
"""
from ..db_connector import base_relation_class
#>>> PLACE YOUR CODE BELLOW THIS LINE. DO NOT REMOVE THIS LINE!
#<<< PLACE YOUR CODE ABOVE THIS LINE. DO NOT REMOVE THIS LINE!
__RCLS = base_relation_class('api.domain')
class Domain( __RCLS):
"""
__RCLS: <class 'half_orm.relation.Table_ApiApiDomain'>
This class allows you to manipulate the data in the PG relation:
TABLE: "api"."api"."domain"
FIELDS:
- version: (text) PK
- name: (text) PK
FOREIGN KEYS:
- _reverse_fkey_api_api_acl_function_version_domain: (version, name)
"api"."api"."acl_function"(version, domain)
- domain_version_fkey: (version)
"api"."api"."version"(name)
- _reverse_fkey_api_api_route_version_domain: (version, name)
"api"."api"."route"(version, domain)
"""
def __init__(self, **kwargs):
super(Domain, self).__init__(**kwargs)
#>>> PLACE YOUR CODE BELLOW THIS LINE. DO NOT REMOVE THIS LINE!

View File

@ -1,49 +0,0 @@
#-*- coding: utf-8 -*-
# pylint: disable=wrong-import-order
"""The module apidb.api.route povides the Route class.
WARNING!
This file is part of the apidb package. It has been generated by the
command halfORM. To keep it in sync with your database structure, just rerun
halfORM.
More information on the half_orm library on https://github.com/collorg/halfORM.
DO NOT REMOVE OR MODIFY THE LINES BEGINING WITH:
#>>> PLACE YOUR CODE BELOW...
#<<< PLACE YOUR CODE ABOVE...
MAKE SURE YOU PLACE YOUR CODE BETWEEN THESE LINES OR AT THE END OF THE FILE.
halfORM ONLY PRESERVES THE CODE BETWEEN THESE MARKS WHEN IT IS RUN.
"""
from ..db_connector import base_relation_class
#>>> PLACE YOUR CODE BELLOW THIS LINE. DO NOT REMOVE THIS LINE!
#<<< PLACE YOUR CODE ABOVE THIS LINE. DO NOT REMOVE THIS LINE!
__RCLS = base_relation_class('api.route')
class Route( __RCLS):
"""
__RCLS: <class 'half_orm.relation.Table_ApiApiRoute'>
This class allows you to manipulate the data in the PG relation:
TABLE: "api"."api"."route"
FIELDS:
- path: (text) PK
- version: (text) PK
- domain: (text) PK
FOREIGN KEYS:
- _reverse_fkey_api_api_acl_path_version_domain: (path, version, domain)
"api"."api"."acl"(path, version, domain)
- route_domain_fkey: (version, domain)
"api"."api"."domain"(version, name)
"""
def __init__(self, **kwargs):
super(Route, self).__init__(**kwargs)
#>>> PLACE YOUR CODE BELLOW THIS LINE. DO NOT REMOVE THIS LINE!

View File

@ -1,47 +0,0 @@
#-*- coding: utf-8 -*-
# pylint: disable=wrong-import-order
"""The module apidb.api.version povides the Version class.
WARNING!
This file is part of the apidb package. It has been generated by the
command halfORM. To keep it in sync with your database structure, just rerun
halfORM.
More information on the half_orm library on https://github.com/collorg/halfORM.
DO NOT REMOVE OR MODIFY THE LINES BEGINING WITH:
#>>> PLACE YOUR CODE BELOW...
#<<< PLACE YOUR CODE ABOVE...
MAKE SURE YOU PLACE YOUR CODE BETWEEN THESE LINES OR AT THE END OF THE FILE.
halfORM ONLY PRESERVES THE CODE BETWEEN THESE MARKS WHEN IT IS RUN.
"""
from ..db_connector import base_relation_class
#>>> PLACE YOUR CODE BELLOW THIS LINE. DO NOT REMOVE THIS LINE!
#<<< PLACE YOUR CODE ABOVE THIS LINE. DO NOT REMOVE THIS LINE!
__RCLS = base_relation_class('api.version')
class Version( __RCLS):
"""
__RCLS: <class 'half_orm.relation.Table_ApiApiVersion'>
This class allows you to manipulate the data in the PG relation:
TABLE: "api"."api"."version"
FIELDS:
- name: (text) PK
- server: (cidr) NOT NULL
- port: (int4) NOT NULL
FOREIGN KEY:
- _reverse_fkey_api_api_domain_version: (name)
"api"."api"."domain"(version)
"""
def __init__(self, **kwargs):
super(Version, self).__init__(**kwargs)
#>>> PLACE YOUR CODE BELLOW THIS LINE. DO NOT REMOVE THIS LINE!

View File

@ -1,11 +0,0 @@
"""This file is part of the apidb package. It has been generated by the
command halfORM. To keep it in sync with your database structure, just rerun
halfORM.
More information on the half_orm library on https://github.com/collorg/halfORM.
"""
__all__ = [
'acl',
'route'
]

View File

@ -1,49 +0,0 @@
#-*- coding: utf-8 -*-
# pylint: disable=wrong-import-order
"""The module apidb.api.view.acl povides the Acl class.
WARNING!
This file is part of the apidb package. It has been generated by the
command halfORM. To keep it in sync with your database structure, just rerun
halfORM.
More information on the half_orm library on https://github.com/collorg/halfORM.
DO NOT REMOVE OR MODIFY THE LINES BEGINING WITH:
#>>> PLACE YOUR CODE BELOW...
#<<< PLACE YOUR CODE ABOVE...
MAKE SURE YOU PLACE YOUR CODE BETWEEN THESE LINES OR AT THE END OF THE FILE.
halfORM ONLY PRESERVES THE CODE BETWEEN THESE MARKS WHEN IT IS RUN.
"""
from ...db_connector import base_relation_class
#>>> PLACE YOUR CODE BELLOW THIS LINE. DO NOT REMOVE THIS LINE!
#<<< PLACE YOUR CODE ABOVE THIS LINE. DO NOT REMOVE THIS LINE!
__RCLS = base_relation_class('api.view.acl')
class Acl( __RCLS):
"""
__RCLS: <class 'half_orm.relation.View_ApiApiviewAcl'>
This class allows you to manipulate the data in the PG relation:
VIEW: "api"."api.view"."acl"
FIELDS:
- name: (text)
- http_verb: (verb)
- path: (text)
- version: (text)
- domain: (text)
- function: (text)
- acl_function_name: (text)
- abs_path: (text)
"""
def __init__(self, **kwargs):
super(Acl, self).__init__(**kwargs)
#>>> PLACE YOUR CODE BELLOW THIS LINE. DO NOT REMOVE THIS LINE!

View File

@ -1,48 +0,0 @@
#-*- coding: utf-8 -*-
# pylint: disable=wrong-import-order
"""The module apidb.api.view.route povides the Route class.
WARNING!
This file is part of the apidb package. It has been generated by the
command halfORM. To keep it in sync with your database structure, just rerun
halfORM.
More information on the half_orm library on https://github.com/collorg/halfORM.
DO NOT REMOVE OR MODIFY THE LINES BEGINING WITH:
#>>> PLACE YOUR CODE BELOW...
#<<< PLACE YOUR CODE ABOVE...
MAKE SURE YOU PLACE YOUR CODE BETWEEN THESE LINES OR AT THE END OF THE FILE.
halfORM ONLY PRESERVES THE CODE BETWEEN THESE MARKS WHEN IT IS RUN.
"""
from ...db_connector import base_relation_class
#>>> PLACE YOUR CODE BELLOW THIS LINE. DO NOT REMOVE THIS LINE!
#<<< PLACE YOUR CODE ABOVE THIS LINE. DO NOT REMOVE THIS LINE!
__RCLS = base_relation_class('api.view.route')
class Route( __RCLS):
"""
__RCLS: <class 'half_orm.relation.View_ApiApiviewRoute'>
This class allows you to manipulate the data in the PG relation:
VIEW: "api"."api.view"."route"
FIELDS:
- path: (text)
- version: (text)
- domain: (text)
- name: (text)
- server: (cidr)
- port: (int4)
- abs_path: (text)
"""
def __init__(self, **kwargs):
super(Route, self).__init__(**kwargs)
#>>> PLACE YOUR CODE BELLOW THIS LINE. DO NOT REMOVE THIS LINE!

View File

@ -1,17 +0,0 @@
#-*- coding: utf-8 -*-
"""This module exports the fonction base_relation_class which is
imported by all modules in the package apidb.
"""
from half_orm.model import Model
__all__ = ['base_relation_class']
MODEL = Model('api', scope=__name__)
def base_relation_class(qrn):
"""Returns the class corresponding to the QRN (qualified relation name).
"""
cls = MODEL.get_relation_class(qrn)
return cls

View File

View File

@ -0,0 +1,152 @@
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]

View File

@ -0,0 +1,3 @@
pyexcel>=0.6.3,<1
pyexcel-ods>=0.5.6,<1
pyexcel-xlsx=0.6.0,<1

647
poetry.lock generated
View File

@ -1,647 +0,0 @@
[[package]]
category = "dev"
description = "Atomic file writes."
marker = "sys_platform == \"win32\""
name = "atomicwrites"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.4.0"
[[package]]
category = "dev"
description = "Classes Without Boilerplate"
name = "attrs"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "19.3.0"
[package.extras]
azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"]
dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"]
docs = ["sphinx", "zope.interface"]
tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
[[package]]
category = "dev"
description = "Python package for providing Mozilla's CA Bundle."
name = "certifi"
optional = false
python-versions = "*"
version = "2020.6.20"
[[package]]
category = "dev"
description = "Universal encoding detector for Python 2 and 3"
name = "chardet"
optional = false
python-versions = "*"
version = "3.0.4"
[[package]]
category = "main"
description = "Composable command line interface toolkit"
name = "click"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "7.1.2"
[[package]]
category = "dev"
description = "Cross-platform colored terminal text."
marker = "sys_platform == \"win32\""
name = "colorama"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.4.3"
[[package]]
category = "main"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
name = "fastapi"
optional = true
python-versions = ">=3.6"
version = "0.58.1"
[package.dependencies]
pydantic = ">=0.32.2,<2.0.0"
starlette = "0.13.4"
[package.extras]
all = ["requests", "aiofiles", "jinja2", "python-multipart", "itsdangerous", "pyyaml", "graphene", "ujson", "orjson", "email-validator", "uvicorn", "async-exit-stack", "async-generator"]
dev = ["pyjwt", "passlib", "autoflake", "flake8", "uvicorn", "graphene"]
doc = ["mkdocs", "mkdocs-material", "markdown-include", "typer", "typer-cli", "pyyaml"]
test = ["pytest (5.4.3)", "pytest-cov (2.10.0)", "mypy", "black", "isort", "requests", "email-validator", "sqlalchemy", "peewee", "databases", "orjson", "async-exit-stack", "async-generator", "python-multipart", "aiofiles", "flask"]
[[package]]
category = "main"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
name = "h11"
optional = false
python-versions = "*"
version = "0.9.0"
[[package]]
category = "main"
description = "A simple ORM in Python only dealing with the DML part of SQL."
name = "half-orm"
optional = false
python-versions = "*"
version = "0.2.0"
[package.dependencies]
PyYAML = "*"
psycopg2-binary = "*"
[package.source]
reference = "fe53195abb637d2192857e8b4878f4865b0fcce4"
type = "git"
url = "git@gite.lirmm.fr:newsi/halfORM.git"
[[package]]
category = "main"
description = "A collection of framework independent HTTP protocol utils."
marker = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\""
name = "httptools"
optional = false
python-versions = "*"
version = "0.1.1"
[package.extras]
test = ["Cython (0.29.14)"]
[[package]]
category = "dev"
description = "Internationalized Domain Names in Applications (IDNA)"
name = "idna"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.10"
[[package]]
category = "dev"
description = "Read metadata from Python packages"
marker = "python_version < \"3.8\""
name = "importlib-metadata"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
version = "1.7.0"
[package.dependencies]
zipp = ">=0.5"
[package.extras]
docs = ["sphinx", "rst.linker"]
testing = ["packaging", "pep517", "importlib-resources (>=1.3)"]
[[package]]
category = "dev"
description = "More routines for operating on iterables, beyond itertools"
name = "more-itertools"
optional = false
python-versions = ">=3.5"
version = "8.4.0"
[[package]]
category = "main"
description = ""
name = "organigramme"
optional = true
python-versions = "^3.7"
version = "0.1.0"
[package.dependencies]
fastapi = "^0"
half-orm = "branch master"
halfapi = "branch master"
sidb = "branch master"
starlette = "^0"
uvicorn = "^0"
[package.source]
reference = "f26fc6dcde165bfbb88d4664f842de33aacfb1f8"
type = "git"
url = "git@gite.lirmm.fr:newsi/api/organigramme.git"
[[package]]
category = "dev"
description = "Core utilities for Python packages"
name = "packaging"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "20.4"
[package.dependencies]
pyparsing = ">=2.0.2"
six = "*"
[[package]]
category = "dev"
description = "plugin and hook calling mechanisms for python"
name = "pluggy"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.13.1"
[package.dependencies]
[package.dependencies.importlib-metadata]
python = "<3.8"
version = ">=0.12"
[package.extras]
dev = ["pre-commit", "tox"]
[[package]]
category = "main"
description = "psycopg2 - Python-PostgreSQL Database Adapter"
name = "psycopg2-binary"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "2.8.5"
[[package]]
category = "dev"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
name = "py"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "1.9.0"
[[package]]
category = "main"
description = "Data validation and settings management using python 3.6 type hinting"
name = "pydantic"
optional = true
python-versions = ">=3.6"
version = "1.5.1"
[package.extras]
dotenv = ["python-dotenv (>=0.10.4)"]
email = ["email-validator (>=1.0.3)"]
typing_extensions = ["typing-extensions (>=3.7.2)"]
[[package]]
category = "main"
description = "JSON Web Token implementation in Python"
name = "pyjwt"
optional = false
python-versions = "*"
version = "1.7.1"
[package.extras]
crypto = ["cryptography (>=1.4)"]
flake8 = ["flake8", "flake8-import-order", "pep8-naming"]
test = ["pytest (>=4.0.1,<5.0.0)", "pytest-cov (>=2.6.0,<3.0.0)", "pytest-runner (>=4.2,<5.0.0)"]
[[package]]
category = "dev"
description = "Python parsing module"
name = "pyparsing"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
version = "2.4.7"
[[package]]
category = "dev"
description = "pytest: simple powerful testing with Python"
name = "pytest"
optional = false
python-versions = ">=3.5"
version = "5.4.3"
[package.dependencies]
atomicwrites = ">=1.0"
attrs = ">=17.4.0"
colorama = "*"
more-itertools = ">=4.0.0"
packaging = "*"
pluggy = ">=0.12,<1.0"
py = ">=1.5.0"
wcwidth = "*"
[package.dependencies.importlib-metadata]
python = "<3.8"
version = ">=0.12"
[package.extras]
checkqa-mypy = ["mypy (v0.761)"]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
[[package]]
category = "dev"
description = "py.test plugin that allows you to add environment variables."
name = "pytest-env"
optional = false
python-versions = "*"
version = "0.6.2"
[package.dependencies]
pytest = ">=2.6.0"
[[package]]
category = "main"
description = "Add .env support to your django/flask apps in development and deployments"
name = "python-dotenv"
optional = false
python-versions = "*"
version = "0.14.0"
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
category = "main"
description = "YAML parser and emitter for Python"
name = "pyyaml"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "5.3.1"
[[package]]
category = "dev"
description = "Python HTTP for Humans."
name = "requests"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.24.0"
[package.dependencies]
certifi = ">=2017.4.17"
chardet = ">=3.0.2,<4"
idna = ">=2.5,<3"
urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
[package.extras]
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
[[package]]
category = "main"
description = "Package for si PG"
name = "sidb"
optional = true
python-versions = "*"
version = "0.0.0"
[package.dependencies]
half_orm = "*"
[package.source]
reference = "d0f14a9631eecd29098d13a3f34e9cd533145f24"
type = "git"
url = "git@gite.lirmm.fr:newsi/sidb_halfORM.git"
[[package]]
category = "dev"
description = "Python 2 and 3 compatibility utilities"
name = "six"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
version = "1.15.0"
[[package]]
category = "main"
description = "The little ASGI library that shines."
name = "starlette"
optional = false
python-versions = ">=3.6"
version = "0.13.4"
[package.extras]
full = ["aiofiles", "graphene", "itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests", "ujson"]
[[package]]
category = "dev"
description = "HTTP library with thread-safe connection pooling, file post, and more."
name = "urllib3"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
version = "1.25.9"
[package.extras]
brotli = ["brotlipy (>=0.6.0)"]
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
[[package]]
category = "main"
description = "The lightning-fast ASGI server."
name = "uvicorn"
optional = false
python-versions = "*"
version = "0.11.5"
[package.dependencies]
click = ">=7.0.0,<8.0.0"
h11 = ">=0.8,<0.10"
httptools = ">=0.1.0,<0.2.0"
uvloop = ">=0.14.0"
websockets = ">=8.0.0,<9.0.0"
[package.extras]
watchgodreload = ["watchgod (>=0.6,<0.7)"]
[[package]]
category = "main"
description = "Fast implementation of asyncio event loop on top of libuv"
marker = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\""
name = "uvloop"
optional = false
python-versions = "*"
version = "0.14.0"
[[package]]
category = "dev"
description = "Measures the displayed width of unicode strings in a terminal"
name = "wcwidth"
optional = false
python-versions = "*"
version = "0.2.5"
[[package]]
category = "main"
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
name = "websockets"
optional = false
python-versions = ">=3.6.1"
version = "8.1"
[[package]]
category = "dev"
description = "Backport of pathlib-compatible object wrapper for zip files"
marker = "python_version < \"3.8\""
name = "zipp"
optional = false
python-versions = ">=3.6"
version = "3.1.0"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["jaraco.itertools", "func-timeout"]
[extras]
organigramme = ["fastapi", "organigramme"]
[metadata]
content-hash = "b801de5aea1ab2defb9265cc25773b55f7c00e36e7206e19173cef5a0ee99eb4"
python-versions = "^3.7"
[metadata.files]
atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
]
attrs = [
{file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"},
{file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"},
]
certifi = [
{file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"},
{file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"},
]
chardet = [
{file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
{file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
]
click = [
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
]
colorama = [
{file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
]
fastapi = [
{file = "fastapi-0.58.1-py3-none-any.whl", hash = "sha256:d7499761d5ca901cdf5b6b73018d14729593f8ab1ea22d241f82fa574fc406ad"},
{file = "fastapi-0.58.1.tar.gz", hash = "sha256:92e59b77eef7d6eaa80b16d275adda06b5f33b12d777e3fc5521b2f7f4718e13"},
]
h11 = [
{file = "h11-0.9.0-py2.py3-none-any.whl", hash = "sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"},
{file = "h11-0.9.0.tar.gz", hash = "sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1"},
]
half-orm = []
httptools = [
{file = "httptools-0.1.1-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce"},
{file = "httptools-0.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4"},
{file = "httptools-0.1.1-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6"},
{file = "httptools-0.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c"},
{file = "httptools-0.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a"},
{file = "httptools-0.1.1-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f"},
{file = "httptools-0.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2"},
{file = "httptools-0.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009"},
{file = "httptools-0.1.1-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437"},
{file = "httptools-0.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d"},
{file = "httptools-0.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be"},
{file = "httptools-0.1.1.tar.gz", hash = "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce"},
]
idna = [
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
]
importlib-metadata = [
{file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"},
{file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"},
]
more-itertools = [
{file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"},
{file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"},
]
organigramme = []
packaging = [
{file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"},
{file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"},
]
pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
]
psycopg2-binary = [
{file = "psycopg2-binary-2.8.5.tar.gz", hash = "sha256:ccdc6a87f32b491129ada4b87a43b1895cf2c20fdb7f98ad979647506ffc41b6"},
{file = "psycopg2_binary-2.8.5-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:96d3038f5bd061401996614f65d27a4ecb62d843eb4f48e212e6d129171a721f"},
{file = "psycopg2_binary-2.8.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:08507efbe532029adee21b8d4c999170a83760d38249936038bd0602327029b5"},
{file = "psycopg2_binary-2.8.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:b9a8b391c2b0321e0cd7ec6b4cfcc3dd6349347bd1207d48bcb752aa6c553a66"},
{file = "psycopg2_binary-2.8.5-cp27-cp27m-win32.whl", hash = "sha256:3286541b9d85a340ee4ed42732d15fc1bb441dc500c97243a768154ab8505bb5"},
{file = "psycopg2_binary-2.8.5-cp27-cp27m-win_amd64.whl", hash = "sha256:008da3ab51adc70a5f1cfbbe5db3a22607ab030eb44bcecf517ad11a0c2b3cac"},
{file = "psycopg2_binary-2.8.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ba13346ff6d3eb2dca0b6fa0d8a9d999eff3dcd9b55f3a890f12b0b6362b2b38"},
{file = "psycopg2_binary-2.8.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:c8830b7d5f16fd79d39b21e3d94f247219036b29b30c8270314c46bf8b732389"},
{file = "psycopg2_binary-2.8.5-cp34-cp34m-win32.whl", hash = "sha256:51f7823f1b087d2020d8e8c9e6687473d3d239ba9afc162d9b2ab6e80b53f9f9"},
{file = "psycopg2_binary-2.8.5-cp34-cp34m-win_amd64.whl", hash = "sha256:107d9be3b614e52a192719c6bf32e8813030020ea1d1215daa86ded9a24d8b04"},
{file = "psycopg2_binary-2.8.5-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:930315ac53dc65cbf52ab6b6d27422611f5fb461d763c531db229c7e1af6c0b3"},
{file = "psycopg2_binary-2.8.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6bb2dd006a46a4a4ce95201f836194eb6a1e863f69ee5bab506673e0ca767057"},
{file = "psycopg2_binary-2.8.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:3939cf75fc89c5e9ed836e228c4a63604dff95ad19aed2bbf71d5d04c15ed5ce"},
{file = "psycopg2_binary-2.8.5-cp35-cp35m-win32.whl", hash = "sha256:a20299ee0ea2f9cca494396ac472d6e636745652a64a418b39522c120fd0a0a4"},
{file = "psycopg2_binary-2.8.5-cp35-cp35m-win_amd64.whl", hash = "sha256:cc30cb900f42c8a246e2cb76539d9726f407330bc244ca7729c41a44e8d807fb"},
{file = "psycopg2_binary-2.8.5-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:40abc319f7f26c042a11658bf3dd3b0b3bceccf883ec1c565d5c909a90204434"},
{file = "psycopg2_binary-2.8.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:702f09d8f77dc4794651f650828791af82f7c2efd8c91ae79e3d9fe4bb7d4c98"},
{file = "psycopg2_binary-2.8.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d1a8b01f6a964fec702d6b6dac1f91f2b9f9fe41b310cbb16c7ef1fac82df06d"},
{file = "psycopg2_binary-2.8.5-cp36-cp36m-win32.whl", hash = "sha256:17a0ea0b0eabf07035e5e0d520dabc7950aeb15a17c6d36128ba99b2721b25b1"},
{file = "psycopg2_binary-2.8.5-cp36-cp36m-win_amd64.whl", hash = "sha256:e004db88e5a75e5fdab1620fb9f90c9598c2a195a594225ac4ed2a6f1c23e162"},
{file = "psycopg2_binary-2.8.5-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:a34826d6465c2e2bbe9d0605f944f19d2480589f89863ed5f091943be27c9de4"},
{file = "psycopg2_binary-2.8.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cac918cd7c4c498a60f5d2a61d4f0a6091c2c9490d81bc805c963444032d0dab"},
{file = "psycopg2_binary-2.8.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7b832d76cc65c092abd9505cc670c4e3421fd136fb6ea5b94efbe4c146572505"},
{file = "psycopg2_binary-2.8.5-cp37-cp37m-win32.whl", hash = "sha256:bb0608694a91db1e230b4a314e8ed00ad07ed0c518f9a69b83af2717e31291a3"},
{file = "psycopg2_binary-2.8.5-cp37-cp37m-win_amd64.whl", hash = "sha256:eb2f43ae3037f1ef5e19339c41cf56947021ac892f668765cd65f8ab9814192e"},
{file = "psycopg2_binary-2.8.5-cp38-cp38-macosx_10_9_x86_64.macosx_10_9_intel.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:07cf82c870ec2d2ce94d18e70c13323c89f2f2a2628cbf1feee700630be2519a"},
{file = "psycopg2_binary-2.8.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a69970ee896e21db4c57e398646af9edc71c003bc52a3cc77fb150240fefd266"},
{file = "psycopg2_binary-2.8.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7036ccf715925251fac969f4da9ad37e4b7e211b1e920860148a10c0de963522"},
{file = "psycopg2_binary-2.8.5-cp38-cp38-win32.whl", hash = "sha256:8f74e631b67482d504d7e9cf364071fc5d54c28e79a093ff402d5f8f81e23bfa"},
{file = "psycopg2_binary-2.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:fa466306fcf6b39b8a61d003123d442b23707d635a5cb05ac4e1b62cc79105cd"},
]
py = [
{file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"},
{file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"},
]
pydantic = [
{file = "pydantic-1.5.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2a6904e9f18dea58f76f16b95cba6a2f20b72d787abd84ecd67ebc526e61dce6"},
{file = "pydantic-1.5.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:da8099fca5ee339d5572cfa8af12cf0856ae993406f0b1eb9bb38c8a660e7416"},
{file = "pydantic-1.5.1-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:68dece67bff2b3a5cc188258e46b49f676a722304f1c6148ae08e9291e284d98"},
{file = "pydantic-1.5.1-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:ab863853cb502480b118187d670f753be65ec144e1654924bec33d63bc8b3ce2"},
{file = "pydantic-1.5.1-cp36-cp36m-win_amd64.whl", hash = "sha256:2007eb062ed0e57875ce8ead12760a6e44bf5836e6a1a7ea81d71eeecf3ede0f"},
{file = "pydantic-1.5.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:20a15a303ce1e4d831b4e79c17a4a29cb6740b12524f5bba3ea363bff65732bc"},
{file = "pydantic-1.5.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:473101121b1bd454c8effc9fe66d54812fdc128184d9015c5aaa0d4e58a6d338"},
{file = "pydantic-1.5.1-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:9be755919258d5d168aeffbe913ed6e8bd562e018df7724b68cabdee3371e331"},
{file = "pydantic-1.5.1-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:b96ce81c4b5ca62ab81181212edfd057beaa41411cd9700fbcb48a6ba6564b4e"},
{file = "pydantic-1.5.1-cp37-cp37m-win_amd64.whl", hash = "sha256:93b9f265329d9827f39f0fca68f5d72cc8321881cdc519a1304fa73b9f8a75bd"},
{file = "pydantic-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2c753d355126ddd1eefeb167fa61c7037ecd30b98e7ebecdc0d1da463b4ea09"},
{file = "pydantic-1.5.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8433dbb87246c0f562af75d00fa80155b74e4f6924b0db6a2078a3cd2f11c6c4"},
{file = "pydantic-1.5.1-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:0a1cdf24e567d42dc762d3fed399bd211a13db2e8462af9dfa93b34c41648efb"},
{file = "pydantic-1.5.1-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:8be325fc9da897029ee48d1b5e40df817d97fe969f3ac3fd2434ba7e198c55d5"},
{file = "pydantic-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:3714a4056f5bdbecf3a41e0706ec9b228c9513eee2ad884dc2c568c4dfa540e9"},
{file = "pydantic-1.5.1-py36.py37.py38-none-any.whl", hash = "sha256:70f27d2f0268f490fe3de0a9b6fca7b7492b8fd6623f9fecd25b221ebee385e3"},
{file = "pydantic-1.5.1.tar.gz", hash = "sha256:f0018613c7a0d19df3240c2a913849786f21b6539b9f23d85ce4067489dfacfa"},
]
pyjwt = [
{file = "PyJWT-1.7.1-py2.py3-none-any.whl", hash = "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e"},
{file = "PyJWT-1.7.1.tar.gz", hash = "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"},
]
pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
]
pytest = [
{file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"},
{file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"},
]
pytest-env = [
{file = "pytest-env-0.6.2.tar.gz", hash = "sha256:7e94956aef7f2764f3c147d216ce066bf6c42948bb9e293169b1b1c880a580c2"},
]
python-dotenv = [
{file = "python-dotenv-0.14.0.tar.gz", hash = "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d"},
{file = "python_dotenv-0.14.0-py2.py3-none-any.whl", hash = "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423"},
]
pyyaml = [
{file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"},
{file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"},
{file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"},
{file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"},
{file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"},
{file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"},
{file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"},
{file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"},
{file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"},
{file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"},
{file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"},
]
requests = [
{file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"},
{file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"},
]
sidb = []
six = [
{file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"},
{file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"},
]
starlette = [
{file = "starlette-0.13.4-py3-none-any.whl", hash = "sha256:0fb4b38d22945b46acb880fedee7ee143fd6c0542992501be8c45c0ed737dd1a"},
{file = "starlette-0.13.4.tar.gz", hash = "sha256:04fe51d86fd9a594d9b71356ed322ccde5c9b448fc716ac74155e5821a922f8d"},
]
urllib3 = [
{file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"},
{file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"},
]
uvicorn = [
{file = "uvicorn-0.11.5-py3-none-any.whl", hash = "sha256:50577d599775dac2301bac8bd5b540d19a9560144143c5bdab13cba92783b6e7"},
{file = "uvicorn-0.11.5.tar.gz", hash = "sha256:596eaa8645b6dbc24d6610e335f8ddf5f925b4c4b86fdc7146abb0bf0da65d17"},
]
uvloop = [
{file = "uvloop-0.14.0-cp35-cp35m-macosx_10_11_x86_64.whl", hash = "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd"},
{file = "uvloop-0.14.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726"},
{file = "uvloop-0.14.0-cp36-cp36m-macosx_10_11_x86_64.whl", hash = "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7"},
{file = "uvloop-0.14.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"},
{file = "uvloop-0.14.0-cp37-cp37m-macosx_10_11_x86_64.whl", hash = "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891"},
{file = "uvloop-0.14.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95"},
{file = "uvloop-0.14.0-cp38-cp38-macosx_10_11_x86_64.whl", hash = "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5"},
{file = "uvloop-0.14.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09"},
{file = "uvloop-0.14.0.tar.gz", hash = "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e"},
]
wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
]
websockets = [
{file = "websockets-8.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c"},
{file = "websockets-8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170"},
{file = "websockets-8.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8"},
{file = "websockets-8.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb"},
{file = "websockets-8.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5"},
{file = "websockets-8.1-cp36-cp36m-win32.whl", hash = "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a"},
{file = "websockets-8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5"},
{file = "websockets-8.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989"},
{file = "websockets-8.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d"},
{file = "websockets-8.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779"},
{file = "websockets-8.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8"},
{file = "websockets-8.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422"},
{file = "websockets-8.1-cp37-cp37m-win32.whl", hash = "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc"},
{file = "websockets-8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308"},
{file = "websockets-8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092"},
{file = "websockets-8.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485"},
{file = "websockets-8.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1"},
{file = "websockets-8.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55"},
{file = "websockets-8.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824"},
{file = "websockets-8.1-cp38-cp38-win32.whl", hash = "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36"},
{file = "websockets-8.1-cp38-cp38-win_amd64.whl", hash = "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"},
{file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"},
]
zipp = [
{file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"},
{file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"},
]

View File

@ -1,32 +1,5 @@
[tool.poetry]
name = "halfapi"
version = "0.1.0"
description = "The core module for an halfORM API"
authors = ["Joël Maizi <joel.maizi@lirmm.fr>", "Maxime Alves <maxime.alves@lirmm.fr>"]
homepage = "https://gite.lirmm.fr/newsi/api/halfapi"
[tool.poetry.dependencies]
click = "^7"
half-orm = { git = "git@gite.lirmm.fr:newsi/halfORM.git" }
PyJWT = "^1"
python = "^3.7"
starlette = "^0"
uvicorn = { version = "^0" }
fastapi = { version = "^0", optional = true }
organigramme = { git = "git@gite.lirmm.fr:newsi/api/organigramme.git", optional = true }
python-dotenv = "^0.14.0"
[tool.poetry.dev-dependencies]
pytest = "^5"
requests = "^2"
pytest-env = "^0.6.2"
[tool.poetry.extras]
organigramme = [ "fastapi", "organigramme" ]
[tool.poetry.scripts]
halfapi = 'halfapi.cli:cli'
[build-system] [build-system]
requires = ["poetry>=0.12"] # These are the assumed default build requirements from pip:
build-backend = "poetry.masonry.api" # https://pip.pypa.io/en/stable/reference/pip/#pep-517-and-518-support
requires = ["setuptools>=40.8.0", "wheel"]
build-backend = "setuptools.build_meta"

View File

@ -1,6 +1,5 @@
[pytest] [pytest]
testpaths = tests halfapi testpaths = tests halfapi
addopts = --doctest-modules addopts = --doctest-modules
doctest_optionflags = ELLIPSIS doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL
env = pythonpath = ./tests
DEBUG=TRUE

View File

@ -1,5 +1,61 @@
starlette alog==0.9.13
uvicorn anyio==3.4.0
jwt asgiref==3.4.1
half_orm @ git+ssh://git@gite.lirmm.fr/newsi/halfORM.git astroid==2.9.0
organigramme @ git+ssh://git@gite.lirmm.fr/api/newsi/halfapi.git 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

9
scripts/get_token.py Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/python3
import requests
import json
r = requests.post('http://127.0.0.1:3000/auth',
data={'email':'dhenaut', 'password':'a'})
r_obj = json.loads(r.text)
token = r_obj['token']
print(token)

1
scripts/whoami.sh Normal file
View File

@ -0,0 +1 @@
http 127.0.0.1:3000/halfapi/whoami Authorization:$(http 127.0.0.1:3000/authentication/check email=malves password=papa|jq -r '.token')

2
setup.cfg Normal file
View File

@ -0,0 +1,2 @@
[metadata]
license_files = LICENSE

94
setup.py Executable file
View File

@ -0,0 +1,94 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import re
from setuptools import setup, find_packages
import pathlib
here = pathlib.Path(__file__).parent.resolve()
long_description = (here / 'README.md').read_text(encoding='utf-8')
def get_version(package):
"""
Return package version as listed in `__version__` in `init.py`.
"""
with open(os.path.join(package, "__init__.py")) as f:
return re.search("__version__ = ['\"]([^'\"]+)['\"]", f.read()).group(1)
def get_packages(package):
"""
Return root package and all sub-packages.
"""
return [
dirpath
for dirpath, dirnames, filenames in os.walk(package)
if os.path.exists(os.path.join(dirpath, "__init__.py"))
]
module_name="halfapi"
setup(
name=module_name,
version=get_version(module_name),
url="https://github.com/halfAPI/halfapi",
description="Core to write deep APIs using a module's tree",
author="Maxime ALVES",
author_email="maxime@freepoteries.fr",
license="GPLv3",
long_description=long_description,
long_description_content_type="text/markdown",
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",
"uvicorn>=0.13,<1",
"orjson>=3.8.5,<4",
"pyyaml>=6,<7",
"timing-asgi>=0.2.1,<1",
"schema>=0.7.4,<1",
"toml>=0.10,<0.11",
"packaging>=19.0",
"python-multipart"
],
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",
"requests",
"httpx",
"openapi-schema-validator",
"openapi-spec-validator",
"coverage"
],
"pyexcel":[
"pyexcel",
"pyexcel-ods3",
"pyexcel-xlsx"
]
},
entry_points={
"console_scripts":[
"halfapi=halfapi.cli.cli:cli"
]
},
keywords="web-api development boilerplate",
project_urls={
"Source": "https://github.com/halfAPI/halfapi",
}
)

93
tests/cli/test_cli.py Normal file
View File

@ -0,0 +1,93 @@
#!/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
from halfapi import __version__
from halfapi.cli import cli
Cli = cli.cli
PROJNAME = os.environ.get('PROJ','tmp_api')
def test_options(runner):
# Wrong command
with runner.isolated_filesystem():
r = runner.invoke(Cli, ['foobar'])
assert r.exit_code == 2
# Test existing commands
with runner.isolated_filesystem():
r = runner.invoke(Cli, ['--help'])
assert r.exit_code == 0
with runner.isolated_filesystem():
r = runner.invoke(Cli, ['--version'])
assert r.exit_code == 0
with runner.isolated_filesystem():
r = runner.invoke(Cli, ['init', '--help'])
assert r.exit_code == 0
@pytest.mark.skip
def test_init_project_fail(runner):
# Missing argument (project)
testproject = 'testproject'
r = runner.invoke(Cli, ['init'])
assert r.exit_code == 2
with runner.isolated_filesystem():
# Fail : Wrong project name
r = runner.invoke(Cli, ['init', 'test*-project'])
assert r.exit_code == 1
with runner.isolated_filesystem():
# Fail : Already existing folder
os.mkdir(testproject)
r = runner.invoke(Cli, ['init', testproject])
assert r.exit_code == 1
with runner.isolated_filesystem():
# Fail : Already existing nod
os.mknod(testproject)
r = runner.invoke(Cli, ['init', testproject])
assert r.exit_code == 1
@pytest.mark.skip
def test_init_project(runner):
"""
"""
cp = ConfigParser()
with runner.isolated_filesystem():
env = {
'HALFAPI_CONF_DIR': '.halfapi'
}
res = runner.invoke(Cli, ['init', PROJNAME], env=env)
try:
assert os.path.isdir(PROJNAME)
assert os.path.isdir(os.path.join(PROJNAME, '.halfapi'))
# .halfapi/config check
assert os.path.isfile(os.path.join(PROJNAME, '.halfapi', 'config'))
cp.read(os.path.join(PROJNAME, '.halfapi', 'config'))
assert cp.has_section('project')
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')
except AssertionError as exc:
subprocess.run(['tree', '-a', os.getcwd()])
raise exc
assert res.exit_code == 0
assert res.exception is None

View File

@ -0,0 +1,9 @@
from halfapi.cli.cli import cli
from configparser import ConfigParser
def test_config(cli_runner):
with cli_runner.isolated_filesystem():
result = cli_runner.invoke(cli, ['config'])
cp = ConfigParser()
cp.read_string(result.output)
assert cp.has_section('project')

View File

@ -0,0 +1,84 @@
#!/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

33
tests/cli/test_cli_run.py Normal file
View File

@ -0,0 +1,33 @@
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
"""
def test_run_empty_project(cli_runner):
with cli_runner.isolated_filesystem():
os.mkdir('dummy_domain')
result = cli_runner.invoke(cli, ['run', './dummy_domain'])
assert result.exit_code == 1
def test_run_dummy_project(project_runner):
with patch('uvicorn.run', autospec=True) as runMock:
result = project_runner.invoke(cli, ['run'])
runMock.assert_called_once()
"""

322
tests/conftest.py Normal file
View File

@ -0,0 +1,322 @@
#!/usr/bin/env python3
import logging
import functools
import re
import os
import subprocess
import importlib
import tempfile
from typing import Dict, Tuple
from uuid import uuid1, uuid4, UUID
from click.testing import CliRunner
import jwt
import sys
from unittest.mock import patch
import pytest
from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.middleware.authentication import AuthenticationMiddleware
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.domain import domain, create_domain
from halfapi.lib.responses import ORJSONResponse
from halfapi.lib.jwt_middleware import JWTAuthenticationBackend
logger = logging.getLogger()
PROJNAME = os.environ.get('PROJ','tmp_api')
SECRET = 'dummysecret'
from halfapi.lib.jwt_middleware import (
JWTUser, JWTAuthenticationBackend,
JWTWebSocketAuthenticationBackend)
@pytest.fixture
def dummy_domain():
yield {
'name': 'dummy_domain',
'router': '.routers',
'enabled': True,
'prefix': False,
'config': {
'test': True
}
}
@pytest.fixture
def token_builder():
yield jwt.encode({
'name':'xxx',
'user_id': str(uuid4())},
key=SECRET
)
@pytest.fixture
def token_debug_false_builder():
yield jwt.encode({
'name':'xxx',
'user_id': str(uuid4()),
'debug': False},
key=SECRET
)
@pytest.fixture
def token_debug_true_builder():
yield jwt.encode({
'name':'xxx',
'user_id': str(uuid4()),
'debug': True},
key=SECRET
)
@pytest.fixture
def runner():
return CliRunner()
@pytest.fixture
def cli_runner():
"""Yield a click.testing.CliRunner to invoke the 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)
cli_runner_ = class_()
yield cli_runner_
@pytest.fixture
def halfapicli(cli_runner):
def caller(*args):
return cli_runner.invoke(cli, ' '.join(args))
yield caller
# store history of failures per test class name and per index in parametrize (if
# parametrize used)
_test_failed_incremental: Dict[str, Dict[Tuple[int, ...], str]] = {}
def pytest_runtest_makereport(item, call):
if "incremental" in item.keywords:
# incremental marker is used
if call.excinfo is not None:
# the test has failed
# retrieve the class name of the test
cls_name = str(item.cls)
# retrieve the index of the test (if parametrize is used in
# combination with incremental)
parametrize_index = (
tuple(item.callspec.indices.values())
if hasattr(item, "callspec")
else ()
)
# retrieve the name of the test function
test_name = item.originalname or item.name
# store in _test_failed_incremental the original name of the failed
# test
_test_failed_incremental.setdefault(cls_name, {}).setdefault(
parametrize_index, test_name
)
def pytest_runtest_setup(item):
if "incremental" in item.keywords:
# retrieve the class name of the test
cls_name = str(item.cls)
# check if a previous test has failed for this class
if cls_name in _test_failed_incremental:
# retrieve the index of the test (if parametrize is used in
# combination with incremental)
parametrize_index = (
tuple(item.callspec.indices.values())
if hasattr(item, "callspec")
else ()
)
# retrieve the name of the first test function to fail for this
# class name and index
test_name = _test_failed_incremental[cls_name].get(parametrize_index, None)
# if name found, test has failed for the combination of class name &
# test name
if test_name is not None:
pytest.xfail("previous test failed ({})".format(test_name))
@pytest.fixture
def project_runner(runner, halfapicli, tree):
with runner.isolated_filesystem():
res = halfapicli('init', PROJNAME)
os.chdir(PROJNAME)
fs_path = os.getcwd()
sys.path.insert(0, fs_path)
secret = tempfile.mkstemp()
SECRET_PATH = secret[1]
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)
"""
###
# add dummy domain
###
create_domain('test_domain', 'test_domain.routers')
###
yield halfapicli
while fs_path in sys.path:
sys.path.remove(fs_path)
@pytest.fixture
def dummy_app():
app = Starlette()
app.add_route('/',
lambda request, *args, **kwargs: PlainTextResponse('Hello test!'))
app.add_middleware(
AuthenticationMiddleware,
backend=JWTAuthenticationBackend(secret_key='dummysecret')
)
return app
@pytest.fixture
def dummy_debug_app():
app = Starlette(debug=True)
app.add_route('/',
lambda request, *args, **kwargs: PlainTextResponse('Hello test!'))
app.add_middleware(
AuthenticationMiddleware,
backend=JWTAuthenticationBackend(secret_key='dummysecret')
)
return app
@pytest.fixture
def test_client(dummy_app):
return TestClient(dummy_app)
@pytest.fixture
def create_route():
def wrapped(domain_path, method, path):
stack = [domain_path, *path.split('/')[1:]]
for i in range(len(stack)):
if len(stack[i]) == 0:
continue
path = os.path.join(*stack[0:i+1])
if os.path.isdir(os.path.join(path)):
continue
os.mkdir(path)
init_path = os.path.join(*stack, '__init__.py')
with open(init_path, 'a+') as f:
f.write(f'\ndef {method}():\n raise NotImplementedError')
return wrapped
@pytest.fixture
def dummy_project():
halfapi_config = tempfile.mktemp()
halfapi_secret = tempfile.mktemp()
domain = 'dummy_domain'
with open(halfapi_config, 'w') as f:
f.writelines([
'[project]\n',
'name = lirmm_api\n',
'halfapi_version = 0.5.0\n',
f'secret = {halfapi_secret}\n',
'port = 3050\n',
'loglevel = debug\n',
'[domain.dummy_domain]\n',
f'name = {domain}\n',
'router = dummy_domain.routers\n',
f'[domain.dummy_domain.config]\n',
'test = True'
])
with open(halfapi_secret, 'w') as f:
f.write('turlututu')
return (halfapi_config, 'dummy_domain', 'dummy_domain.routers')
@pytest.fixture
def application_debug(project_runner):
halfAPI = HalfAPI({
'secret':'turlututu',
'production':False,
'domain': {
'dummy_domain': {
'name': 'dummy_domain',
'router': '.routers',
'enabled': True,
'prefix': False,
'config':{
'test': True
}
}
},
})
assert isinstance(halfAPI, HalfAPI)
yield halfAPI.application
@pytest.fixture
def application_domain(dummy_domain):
return HalfAPI({
'secret':'turlututu',
'production':True,
'domain': {
'dummy_domain': {
**dummy_domain,
'config': {
'test': True
}
}
}
}).application
@pytest.fixture
def tree():
def wrapped(path):
list_dirs = os.walk(path)
for root, dirs, files in list_dirs:
for d in dirs:
print(os.path.join(root, d))
for f in files:
print(os.path.join(root, f))
return wrapped

View File

@ -0,0 +1,27 @@
from halfapi import __version__ as halfapi_version
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'}
}
}
}
}
}

13
tests/dummy_domain/acl.py Normal file
View File

@ -0,0 +1,13 @@
from halfapi.lib import acl
from halfapi.lib.acl import public, private, ACLS
from random import randint
def random(*args):
""" Random access ACL
"""
return randint(0,1) == 1
ACLS = (
*ACLS,
('random', random.__doc__, 10)
)

View File

View File

@ -0,0 +1,59 @@
from halfapi.lib import acl
from halfapi.lib.responses import ORJSONResponse
ACLS = {
'GET': [{'acl':acl.public}],
'POST': [{'acl':acl.public}],
'PATCH': [{'acl':acl.public}],
'PUT': [{'acl':acl.public}],
'DELETE': [{'acl':acl.public}]
}
async def get(test):
"""
description:
returns the path parameter
responses:
200:
description: test response
"""
return ORJSONResponse(str(test))
def post(test):
"""
description:
returns the path parameter
responses:
200:
description: test response
"""
return str(test)
def patch(test):
"""
description:
returns the path parameter
responses:
200:
description: test response
"""
return str(test)
def put(test):
"""
description:
returns the path parameter
responses:
200:
description: test response
"""
return str(test)
def delete(test):
"""
description:
returns the path parameter
responses:
200:
description: test response
"""
return str(test)

View File

@ -0,0 +1,14 @@
from starlette.responses import PlainTextResponse
from halfapi.lib import acl
ACLS = {
'GET': [{'acl':acl.public}]
}
async def get(request, *args, **kwargs):
"""
responses:
200:
description: dummy abc.alphabet route
"""
return PlainTextResponse('True')

View File

@ -0,0 +1,21 @@
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"
"""
return {
'id': str(uuid4()),
'name': 'pinnochio',
'nose_size': 42
}

View File

@ -0,0 +1,79 @@
from ... import acl
from halfapi.logging import logger
ACLS = {
'GET' : [
{
'acl':acl.public,
'args': {
'required': {
'foo', 'bar'
},
'optional': {
'x'
}
}
},
{
'acl':acl.random,
'args': {
'required': {
'foo', 'baz'
},
'optional': {
'truebidoo'
}
}
},
],
'POST' : [
{
'acl':acl.private,
'args': {
'required': {
'foo', 'bar'
},
'optional': {
'x'
}
}
},
{
'acl':acl.public,
'args': {
'required': {
'foo', 'baz'
},
'optional': {
'truebidoo',
'z'
}
}
},
]
}
def get(data):
"""
description:
returns the arguments passed in
responses:
200:
description: test response
"""
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

View File

@ -0,0 +1,69 @@
from halfapi.lib.responses import ORJSONResponse, NotImplementedResponse
from ... import acl
ROUTES = {
'abc/alphabet/{test:uuid}': {
'GET': [{'acl': acl.public}]
},
'abc/pinnochio': {
'GET': [{'acl': acl.public}]
},
'config': {
'GET': [{'acl': acl.public}]
},
'arguments': {
'GET': [{
'acl': acl.public,
'args': {
'required': {'foo', 'bar'},
'optional': set()
}
}]
},
}
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'),
'bar': kwargs.get('data').get('bar')
})

View File

@ -0,0 +1,33 @@
from ... import acl
from halfapi.logging import logger
ACLS = {
'GET' : [
{'acl':acl.public},
{'acl':acl.random},
]
}
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']

View File

@ -0,0 +1,8 @@
param_docstring = """
name: second
in: path
description: second parameter description test
required: true
schema:
type: string
"""

View File

@ -0,0 +1,20 @@
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 ''

View File

@ -0,0 +1,13 @@
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'))

View File

@ -0,0 +1,31 @@
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'))

View File

@ -0,0 +1,13 @@
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'))

View File

@ -0,0 +1,4 @@
from . import get
def test_get():
assert isinstance(get(), str)

25
tests/setup.py Normal file
View File

@ -0,0 +1,25 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
def get_packages(package):
"""
Return root package and all sub-packages.
"""
return [
dirpath
for dirpath, dirnames, filenames in os.walk(package)
if os.path.exists(os.path.join(dirpath, "__init__.py"))
]
from setuptools import setup, find_packages
module_name="dummy_domain"
setup(
name=module_name,
version='0',
url="https://gite.lirmm.fr/malves/halfapi",
packages=get_packages(module_name),
python_requires=">=3.7",
install_requires=[]
)

37
tests/test_acl.py Normal file
View File

@ -0,0 +1,37 @@
import pytest
from starlette.responses import PlainTextResponse
from starlette.testclient import TestClient
from halfapi.half_route import HalfRoute
from halfapi.lib import acl
def test_acl_Check(dummy_app, token_debug_false_builder):
"""
A request with ?check should always return a 200 status code
"""
@HalfRoute.acl_decorator(params=[{'acl':acl.public}])
async def test_route_public(request, **kwargs):
raise Exception('Should not raise')
return PlainTextResponse('ok')
dummy_app.add_route('/test_public', test_route_public)
test_client = TestClient(dummy_app)
resp = test_client.request('get', '/test_public?check')
assert resp.status_code == 200
@HalfRoute.acl_decorator(params=[{'acl':acl.private}])
async def test_route_private(request, **kwargs):
raise Exception('Should not raise')
return PlainTextResponse('ok')
dummy_app.add_route('/test_private', test_route_private)
test_client = TestClient(dummy_app)
resp = test_client.request('get', '/test_private')
assert resp.status_code == 401
resp = test_client.request('get', '/test_private?check')
assert resp.status_code == 200

15
tests/test_app.py Normal file
View File

@ -0,0 +1,15 @@
import os
from starlette.applications import Starlette
from unittest.mock import MagicMock, patch
from halfapi.halfapi import HalfAPI
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)

52
tests/test_conf.py Normal file
View File

@ -0,0 +1,52 @@
from unittest import TestCase
import sys
import pytest
from halfapi.halfapi import HalfAPI
class TestConf(TestCase):
def setUp(self):
self.args = {
'domain': {
'dummy_domain': {
'name': 'dummy_domain',
'router': '.routers',
'enabled': True,
'prefix': False,
}
}
}
def tearDown(self):
pass
def test_conf_production_default(self):
halfapi = HalfAPI({
**self.args
})
assert halfapi.PRODUCTION is True
def test_conf_production_true(self):
halfapi = HalfAPI({
**self.args,
'production': True,
})
assert halfapi.PRODUCTION is True
def test_conf_production_false(self):
halfapi = HalfAPI({
**self.args,
'production': False,
})
assert halfapi.PRODUCTION is False
def test_conf_variables(self):
from halfapi.conf import (
CONFIG,
SCHEMA,
)
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)

14
tests/test_constants.py Normal file
View File

@ -0,0 +1,14 @@
from schema import Schema
def test_constants():
from halfapi.lib.constants import (
VERBS,
ACLS_SCHEMA,
ROUTE_SCHEMA,
DOMAIN_SCHEMA,
API_SCHEMA)
assert isinstance(VERBS, tuple)
assert isinstance(ACLS_SCHEMA, Schema)
assert isinstance(ROUTE_SCHEMA, Schema)
assert isinstance(DOMAIN_SCHEMA, Schema)
assert isinstance(API_SCHEMA, Schema)

View File

@ -0,0 +1,67 @@
#!/usr/bin/env python3
import pytest
from starlette.authentication import UnauthenticatedUser
from starlette.testclient import TestClient
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):
# @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')
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')
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')
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')
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')
assert r.status_code == 500
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)

Some files were not shown because too many files have changed in this diff Show More