From 110b8b0969453e4fc6a20359078c5f64ad122b29 Mon Sep 17 00:00:00 2001 From: Maxime Alves LIRMM Date: Tue, 30 Jun 2020 17:50:35 +0200 Subject: [PATCH] commit initial --- README.md | 10 + halfapi/__init__.py | 1 + halfapi/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 167 bytes halfapi/__pycache__/acl.cpython-38.pyc | Bin 0 -> 532 bytes halfapi/__pycache__/app.cpython-38.pyc | Bin 0 -> 4919 bytes halfapi/__pycache__/cli.cpython-38.pyc | Bin 0 -> 1226 bytes halfapi/acl.py | 8 + halfapi/app.py | 221 ++++++++++++++++++ halfapi/cli.py | 48 ++++ halfapi/lib/__pycache__/query.cpython-38.pyc | Bin 0 -> 2931 bytes .../lib/__pycache__/responses.cpython-38.pyc | Bin 0 -> 1864 bytes halfapi/lib/query.py | 87 +++++++ halfapi/lib/responses.py | 35 +++ requirements.txt | 3 + setup.py | 61 +++++ 15 files changed, 474 insertions(+) create mode 100644 README.md create mode 100644 halfapi/__init__.py create mode 100644 halfapi/__pycache__/__init__.cpython-38.pyc create mode 100644 halfapi/__pycache__/acl.cpython-38.pyc create mode 100644 halfapi/__pycache__/app.cpython-38.pyc create mode 100644 halfapi/__pycache__/cli.cpython-38.pyc create mode 100644 halfapi/acl.py create mode 100644 halfapi/app.py create mode 100755 halfapi/cli.py create mode 100644 halfapi/lib/__pycache__/query.cpython-38.pyc create mode 100644 halfapi/lib/__pycache__/responses.cpython-38.pyc create mode 100644 halfapi/lib/query.py create mode 100644 halfapi/lib/responses.py create mode 100644 requirements.txt create mode 100755 setup.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..d4367af --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# halfAPI + +This Python-based ASGI application aims to provide the core functionality to +multiple API domains. + +It's developed at the [LIRMM](https://lirmm.fr) in Montpellier, France. + +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). diff --git a/halfapi/__init__.py b/halfapi/__init__.py new file mode 100644 index 0000000..c57bfd5 --- /dev/null +++ b/halfapi/__init__.py @@ -0,0 +1 @@ +__version__ = '0.0.0' diff --git a/halfapi/__pycache__/__init__.cpython-38.pyc b/halfapi/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f26421b5c72d800aec68ac403ea3f948a28969d2 GIT binary patch literal 167 zcmWIL<>g`kg1u9J#)$#x#~=E$;aE zvecsD%>2Cg_>~MrOh9#D;+MI8Mt*Lperj%JMPjagaZ$2d1}xWhn+Q8Db|OJn5U1b*+=GEL zWaS7q`(+#eEK!ODN&|t@P(~tn#v&BaW0bZ;3)pep9x*3}r{}8?@fCrRaK)S>WyVUv zO;NJEaAYA1X>(4rEHfh{p}b!=r>*@EM(l>(@JKL&RZ1c5<1SnW*< zmFv8ef-ugdb5}0_*O8NIUPsI>ob|Uf;p;HTjFySi`5n`Vvpi9`)jF9mev@QO&4(9f zO=inHVO$NC_jSCiW-8~g*Y!)FV70qBcd#B!;0HRC7EDV@>n^3*h)Vf-htk`Msm-KK ysW2SiXl9JE?Vq*&7PMZ)`ZK*!b7lueVS`NixXD|uo5MV%?i{T(k3H`L9DM^3mV2iF literal 0 HcmV?d00001 diff --git a/halfapi/__pycache__/app.cpython-38.pyc b/halfapi/__pycache__/app.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fdd6c6a6a77ba9a73c4d20adb3ad2502edd855ef GIT binary patch literal 4919 zcmZ`-TW{RP6`movyIg7|tuB_evg5FdlWG-Na)TCa1ILK#*s%j!aHU+?qzh^}vM7G*CxXTB1?PO;|04QdLVeP!2Hy>roTaet)S-PwobskV z*Su+O=8@qnzsbB=@6;paof72(-FaE)V#S-o_-St*;}^V|xA2HLtLQI@mk&5BTfNJ@ zGr!WDHIHwy`q?M6%j$dNCfw=EV8lL`% z8w(OS(OcRO`yCM{5HL4H+>TnYsOxEABkCj~E!=jK<_>Vkt+bO0s zg?|qco6V>#Qe#uJJP7DlS`X9mmHV!rxLbip&8uz@sM^Hsj_SJYw%dN|F4h~n(hRP5 zTFu4{{?@K{Q&ye#HBwUdb!hSW?(6?&mwZD<1H~MC-TvtsXxtAF;mKGkT_{!-AsHu+cB~ z%Ftj!gVnQ+Y542@muOoxcx;XBTCli+S=@s!qdr03kdIH2cWaC@dN&zX&c8r;Rn`3x z8)IMH5zW2W#BWfat}Hy zS8Vv$T2TutVqSL?Nje&ZWfT|8SCxSqr^K_8TN{=1#9tTb6NBT{W*9{1I#wN)WK#Cy3 zBkq+TZSIxizq-?jo>=(ju;5?V!!BFph)wG?Umxk-q@fn|Fr*@X26be4>C1&1nc)7W7V@*)xxU( z%vWVzg}yAK;Q#kc^>Oz#qPaWd=74M`v6Co6u>z5Of5&g`*jKLJvbXctL_8PoM4bTK zhocxl6xg;$d}hCT0{JHzBzeKkiO60W#N3SJnjI-cD;W%bW8ybDE+{)1E%6{3>KX*W zYdL~*=29}@Fb!B!NEv?F$xq4n@Z?vJ;RDq}JG$p?U`xKmGl?C6Dftq-Ej5UT))j=M zjgBWz@ziz4@Sz9I-kR^8%yMCU@})0RY1 ziZaeawZfX>A30A;mtfnwzPO(jV-X;1q{g0j7^g*GoPaXEnMCcIh(WGOJxVF?6KpF? z$?uW6S5P=5uc5d=zCbMzy ziF$=wPc=NVno*t5OkUKwXJO~F=w2Ntt(W8ytztDAVdQlJ&}3R^H1<1gkddYw$FQtW zF+s&^R1onUE66B$5k(7cOevAcRlBaK?Mq8p?1)dKOT-|=+gEAQA5ru;F#5Z`=7A*+ znZl^yX@k04+&8)l2|q-S$7}2}@X8n35#QDOI&kXb><|ptVYhf#LW(qyEqtL9raxf& z2C%yVX%nb^d_ua|)1b9s?Z2J#HJVM6^aTrFM)lVb_J0iTO3T@lz~4&8vuf`0nUBugR{lT8 z_f@u$>R222PCOxHTDUKLTvx_!s-=~HQ&6X!7Plh_93y5bx_BMyDLQeiY}*?!^BKUT z<5^mfpUWRGMa64E_dM5tY=c*xTG|9J@`j=FRV8yzZ*PckdAi~Z(K!zc+5UB`-tbi*7gzeM_YmGl(u>?)kTnuIwcWG zYF#4lZAM}H^501j@kJ50itd>h_fh1=tCxZZ)EHkzWgywL)wz({kUuZyySqGab~jK- zXx);tP$z#x1#M{N1LfhxR$imd>r}jn0*;W#)DRE+q^>!YTx)}tl`oUfh!K_TrQF27 zVJx0N!NAY6IKe6yy2|BG(SuL7MnTiDqE;iHRlg{I0s&=%Ge@23_Dxs`{HBU0v7}9Q zW=Df!a3_zm9QoMLXcbAD=+s8DMnkKFdr7IIGd&s~Tx(>J24;>AWY_puQugOe9UDQq z1E{MDwo1%ZGVV-{Bq&3VC0&=Zz@kD7?NVCA5Y3&WG4dQaMa47<=X9RX3^Ij565`Au z!h2iT9e?$BP?s|_t2!FYPE8%WGdb$c^7`pf$2|mf(j-sO+CL;Mv(JoXbIKcZccmP_ z83(Pb%KTp&u{=#PDND$b28xZTp}a*o8y$bf6eGV;HRU|!rHlYC6NX|+6Oj1p6?o3z cZLr&qP=o$BT4i>5_+fbsSJ0I$vZBme*a literal 0 HcmV?d00001 diff --git a/halfapi/__pycache__/cli.cpython-38.pyc b/halfapi/__pycache__/cli.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b4b73f9ff41959624e69aafeefbd99f9f98b99ac GIT binary patch literal 1226 zcmYjQUyCC}5by5k`IAX-gTw4}Z!(+tM^7iW z7iK|NU;PA!uf3%lBryK??eFg&b3*<^ zamcnZ?|3LuD}If-^(8c}Prh}o|bIf4mCaN%YYXT&8rcZDap@PA^Udm<2>AITNH zqLB}FAVSdK;iHd^rZ6AEUH(1~xQhvh_{Y=fbnB1DLY}S{Uu<2} zHmP-m?qGNzSQ++}KY-FYY9Qz(1L1;Qx@907(D69U}%`sb)Kb(&Z=^VbPo4V9@dFU3aO=Pw(lR`>cc0)FdV}83X%oJwr8zl zNDf+kYkiK%L9@FzH()5Fe|vze02@be?-P&_Hyrb>9N0CTyjsM$+{oN`O6FA}Mxk+m zuyh4IkuEFaG_M*c#zZfTmz9lF+OVoI-a_j1r7-?_lckj^ZLt_9EA_}z$hP&bI(Uf# zjHy1Z^$@U-c7V*;gZDvcrU1m_6sN}kSx7y~-g3tN;|}|mbM_|i2DFE(R@fU8#LzNX z8ONiZx`*zt%+&zl03bSi_RVbm{JVL4@_asjG5hkwxC>RS>!?>JO#^*o+s9U9 bool: + "Unlimited access" + return True diff --git a/halfapi/app.py b/halfapi/app.py new file mode 100644 index 0000000..1d6920a --- /dev/null +++ b/halfapi/app.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +# builtins +import importlib +import sys + +# asgi framework +from starlette.applications import Starlette +from starlette.exceptions import HTTPException +from starlette.middleware import Middleware +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import Response +from starlette.routing import Route, Match, Mount +from starlette.types import ASGIApp, Receive, Scope, Send + +# typing +from typing import Any, Awaitable, Callable, MutableMapping +RequestResponseEndpoint = Callable[ [Request], Awaitable[Response] ] + +# hop-generated classes +from apidb.api.acl_function import AclFunction +from apidb.api.domain import Domain +from apidb.api.route import Route +from apidb.api.view.acl import Acl as AclView +from apidb.api.view.route import Route as RouteView + +# module libraries +from .lib.responses import ForbiddenResponse, NotFoundResponse + +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 object + - 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. + """ + result = { + 'domain': None, + 'name': None, + 'http_verb': None, + 'version': None + } + + try: + """ Identification of the parts of the path + + Examples : + version : v4 + domain : organigramme + path : laboratoire/personnel + """ + _, result['version'], result['domain'], path = scope['path'].split('/', 3) + except ValueError as e: + #404 Not found + raise HTTPException(404) + + # Prefix the path with "/" + path = f'/{path}' + + for route in app.routes: + # Parse all routes + match = route.matches(scope) + if match[0] != Match.FULL: + continue + + 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. + """ + subscope = scope.copy() + subscope['path'] = path + + for mount_route in route.routes: + # Parse all domain routes + submatch = mount_route.matches(subscope) + if submatch[0] != Match.FULL: + continue + + # Route matches + result['name'] = submatch[1]['endpoint'].__name__ + result['http_verb'] = scope['method'] + + 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 + + + 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) + except HTTPException: + return NotFoundResponse() + + d_match, path_params = match_route(app, scope) + + try: + scope['acls'] = [] + """ + for acl in AclView(**d_match).select(): + if ('acl_function_name' not in acl.keys() + or 'domain' not in acl.keys()): + continue + + scope['acls'].append(acl['acl_function_name']) + acl_module = importlib.import_module( + '.acl', + 'organigramme' + ) + + try: + acl_functions.append( + getattr(acl_module.acl, acl_function_name)) + except AttributeError: + + + if True: #function(AUTH, path_params): + response = await call_next(request) + break + """ + except StopIteration: + # TODO : No ACL sur une route existante, prevenir l'admin? + print("No ACL") + pass + + return await self.app(scope, receive, send) + + +def mount_domains(app: Starlette, domains: list): + """ Procedure to mount the registered domains on their prefixes + + Parameters: + + - app (FastAPI): The FastAPI object + - 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: + domain_app = importlib.import_module( + f'{domain["name"]}.app').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 + + # Alter the openapi_url so the /docs page doesn't try to get + # /{name}/openapi.json (@TODO : retport 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 + app.mount('/{version}/{name}'.format(**domain), domain_app) + + +def startup(): + # 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') + sys.stderr.write(str(e)) + sys.exit(1) + +app = Starlette( + middleware=[Middleware(AclCallerMiddleware)], + on_startup=[startup] +) diff --git a/halfapi/cli.py b/halfapi/cli.py new file mode 100755 index 0000000..b33bda5 --- /dev/null +++ b/halfapi/cli.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +import click +import uvicorn +import os +import sys + +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('--host', default='127.0.0.1') +@click.option('--port', default='8000') +@click.option('--debug', default=False) +@click.option('--dev', default=True) +@cli.command() +def run(host, port, debug, dev): + if dev: + debug = True + reload = True + log_level = 'debug' + else: + reload = False + log_level = 'info' + + click.echo('Launching application with default parameters') + click.echo(f'''Parameters : \n + Host : {host} + Port : {port} + Debug : {debug} + Dev : {dev}''') + + sys.path.insert(0, os.getcwd()) + click.echo(sys.path) + uvicorn.run('halfapi.app:app', + host=host, + port=int(port), + log_level=log_level, + reload=reload) + +if __name__ == '__main__': + cli() + diff --git a/halfapi/lib/__pycache__/query.cpython-38.pyc b/halfapi/lib/__pycache__/query.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16dcebd77b041b5dd4e712170679cd16b49d1526 GIT binary patch literal 2931 zcmcguPj4JG6t`z)H=9jTP#dZgie%-|Y?aO~5<+MdR#YK^B8Z}9+d`@kO=i5wWIZz* z+Z&Q-(*ub_dn(BDDiU~p z`}MEyejg{~5BwQ^9QgSZnwp0$AORI5qHC0rzzS?(MfREv<8kf;xu0o}55`V%Yq_AX zN=n67u!59mI`u`pzOuCM^Og>i_!Z5l6N{^lA9B@7V#QNrqq{y%^&T4r1!(wHhtP%O z1<|BKj%h%fj@&t92;qbrQBAegp&jyz9XO}-*gB>!$P*IZc8GSGMfnva`08AcJElL- zZ%J%@LjdYOuKkfTXfPHOP6|g>#{yHvy&HK#iVU8IT(@PcnBL~B5o#UATR_XqV9DkV z_cil;Kal~>=!EUIxjbMkPkIs8Tq?I{A0@jf&b;~e>w9ZGIirI3ShBo-P5Pro@i`;=*iJ%_{rg-rAc*-{~3-R^GV+HR!gh z&;#alvWR?8xhj3#FkhX*o`IdK(cDw_zxvZC`Q(xh>(cZ2rsp@=T$Cuh(0sTx&2`6w zr*!G^$%VZ?a%>vF=CwkX_cE4pM-o_-eORd!v(q9mV~wOOBw)%~g{CmILRx~Xc}iEw znYDaQt7kOLsg?*e4CwwDkyF5Q{&9IbiFlbuA)NHGl73l)GK$J*N*OP)`zsI37m&f! zX4za)ZtEZ|EC?^!47}PR$oUf*Ieh=M=e@^wPCMaCfcbA>%Q28Cp*A3L0z1T;b=Pq8 z7IdHE;BDwG$;<)q1~l~&x_~|>&Ahy;$#WRr!c1;KCcg_Vq669~g@VF1oOMIB`mb>O8ZIFs+nDa zC03qT2oqHaP*K62>KS$R`%byFm&PQXC6NWMqjr!FB+RfYErm?H$uIv+3?l z?A7{YOF2a-5;ursAM-w3xXsN5Kx_Y{*tG=pvFgI5xFls;l z_U%=hkY7=l9k53}f~h$WPB@h$#Fa^!v5-;ZEoo(TXlG97RI)AI%nLnAJ|*1Y?g`

-J)~`|>bcfU|&HV;0KTg_ygo^gkagYz4gT;Oi6_V_y&gq z6JJvySOS9x{{HJDX6QZ}K@|a#*^xD}PAzV6`-F_>9db<15Z#WkPn|RJBUJoz>&P}P zVBZ_DQ~#{0{E-cs+H;H7e`37Bn@=rDz9b)z&&ekFYFrz08=)f(06T3OdOVG#NX%af z3|&RoY{ArvK=#OpeA7IlBYH~C$TRj3-d6>&O-uJtEC)hE4`H3_r6r`92`{Fntgpya zo21D4t8e3tc(qFy8NNFcpJ|3VE3!ypsaJuSd^ViFFO)9wTu2>#w!V4y?()#?B>K_t zih;h>1@2~$^kSujx%FUsZTZ9D<=B`w>184}H-p_&3M`~sORwlsn+=<{i`;-_dHb+0 zh6@wvP71wFrzS1(m%o6MkU}pRpDeweh&ec+d{opFKBBb1aOpl)sSzryjo0F_Rv5t2 zF$Iqg6&B&RYW+Q7s$jJph0UR~ik zbE(T1t^Z+L?Dyf~ML`9NSRe|Msoq3_HKXvZnQXo89@jvDHmZv9sY1EkP)o3sKK?RH znu+l@i-z2&~23z#)!_eAu5D>)qWI~RCU>m)oC4<{>r%ftyXn`mG09fZCOp$ Ha$D}7`dz5) literal 0 HcmV?d00001 diff --git a/halfapi/lib/query.py b/halfapi/lib/query.py new file mode 100644 index 0000000..9150aee --- /dev/null +++ b/halfapi/lib/query.py @@ -0,0 +1,87 @@ +#!/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 +queries +""" + +def parse_query(q: str = ""): + """ + Returns the fitting Response object according to query parameters. + + The parse_query function handles the following arguments in the query + string : format, limit, and offset + It returns a callable function that returns the desired Response object. + + Parameters: + q (str): The query string "q" parameter, in the format + key0:value0|...|keyN:valueN + + Returns: + Callable[[half_orm.model.Model], Response] + + Available query arguments: + format: + - csv + - json + limit: int > 0 + offset: int > 0 + + + Examples: + + >>> parse_query() + .select at 0x...> + + >>> parse_query('format:csv') + .select at 0x...> + + >>> parse_query('format:json') + .select at 0x...> + + >>> parse_query('format:csv|limit:10') + .select at 0x...> + + >>> parse_query('format:csv|offset:10') + .select at 0x...> + + >>> parse_query('format:csv|limit:10|offset:10') + .select at 0x...> + + >>> parse_query('limit:10') + .select at 0x...> + + >>> parse_query('limit=10') + Traceback (most recent call last): + ... + fastapi.exceptions.HTTPException: 400 + + + """ + + params = {} + if len(q) > 0: + try: + split_ = lambda x : x.split(':') + params = dict(map(split_, q.split('|'))) + except ValueError: + raise HTTPException(400) + split_ = lambda x : x.split(':') + params = dict(map(split_, q.split('|'))) + + def select(obj): + + if 'limit' in params and int(params['limit']) > 0: + obj.limit(int(params['limit'])) + + if 'offset' in params and int(params['offset']) > 0: + obj.offset(int(params['offset'])) + + if 'format' in params and params['format'] == 'csv': + return CSVResponse([elt for elt in obj.select()]) + + return [elt for elt in obj.select()] + + return select diff --git a/halfapi/lib/responses.py b/halfapi/lib/responses.py new file mode 100644 index 0000000..0f298fa --- /dev/null +++ b/halfapi/lib/responses.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# builtins +import csv +from datetime import date +from io import TextIOBase, StringIO + +# asgi framework +from starlette.responses import Response + +class NotFoundResponse(Response): + """ The 404 Not Found default Response + """ + def __init__(self): + super().__init__(status_code=404) + +class ForbiddenResponse(Response): + """ The 401 Not Found default Response + """ + def __init__(self): + super().__init__(status_code = 401) + +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) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..69a309f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +starlette +uvicorn +apidb @ git+ssh://git@gite.lirmm.fr/newsi/db/hop_api.git diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..07d6a38 --- /dev/null +++ b/setup.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import re + +from setuptools import setup, find_packages + + +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_long_description(): + """ + Return the README. + """ + with open("README.md", encoding="utf8") as f: + return f.read() + + +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, + python_requires=">=3.7", + version=get_version(module_name), + url="https://gite.lirmm.fr/newsi/api/halfapi", + long_description=get_long_description(), + long_description_content_type="text/markdown", + packages=get_packages(module_name), + package_data={ + 'halfapi': ['lib/*'] + }, + install_requires=[ + "apidb", + "click", + "starlette", + "uvicorn"], + extras_require={ + "tests":["pytest", "requests"] + }, + entry_points={ + "console_scripts":[ + "halfapi=halfapi.cli:cli" + ] + } +)