From 15d40dc65ebac5a5e5094f51e5df94e37c885416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xavier=20dupr=C3=A9?= Date: Mon, 5 Apr 2021 01:12:51 +0200 Subject: [PATCH 1/3] Add file store, example with FastAPI --- _unittests/ut_serverdoc/test_file_store.py | 53 ++++++ .../ut_serverdoc/test_file_store_rest.py | 39 +++++ requirements.txt | 1 + setup.py | 2 + .../ipythonhelper/magic_class_diff.py | 3 +- src/pyquickhelper/server/filestore_fastapi.py | 48 ++++++ src/pyquickhelper/server/filestore_sqlite.py | 161 ++++++++++++++++++ 7 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 _unittests/ut_serverdoc/test_file_store.py create mode 100644 _unittests/ut_serverdoc/test_file_store_rest.py create mode 100644 src/pyquickhelper/server/filestore_fastapi.py create mode 100644 src/pyquickhelper/server/filestore_sqlite.py diff --git a/_unittests/ut_serverdoc/test_file_store.py b/_unittests/ut_serverdoc/test_file_store.py new file mode 100644 index 00000000..9749200f --- /dev/null +++ b/_unittests/ut_serverdoc/test_file_store.py @@ -0,0 +1,53 @@ +""" +@brief test log(time=1s) + +""" +import unittest +import os +import pandas +from pyquickhelper.pycode import ExtTestCase, get_temp_folder +from pyquickhelper.server.filestore_sqlite import SqlLite3FileStore + + +class TestfileStore(ExtTestCase): + + def test_file_store(self): + temp = get_temp_folder(__file__, "temp_file_storage") + name = os.path.join(temp, "filestore.db3") + store = SqlLite3FileStore(name) + df = pandas.DataFrame({"A": ["un", "deux"], "B": [0.5, 0.6]}) + store.add(name="zoo", metadata={'hh': 'kk'}, content=df) + got = list(store.enumerate_content(name="zoo")) + self.assertEqual(len(got), 1) + record = got[0] + name = record['name'] + self.assertEqual(name, "zoo") + self.assertIn("date", record) + content = record['content'] + self.assertIsInstance(df, pandas.DataFrame) + self.assertEqualDataFrame(df, content) + meta = record['metadata'] + self.assertIsInstance(meta, dict) + self.assertEqual(meta, {'hh': 'kk'}) + got = list(store.enumerate(name="zoo")) + self.assertEqual(len(got), 1) + + def test_file_store_exc(self): + temp = get_temp_folder(__file__, "temp_file_storage_exc") + name = os.path.join(temp, "filestore.db3") + store = SqlLite3FileStore(name) + df = pandas.DataFrame({"A": ["un", "deux"], "B": [0.5, 0.6]}) + self.assertRaise( + lambda: store.add(name="zoo", metadata="{'hh': 'kk'}", + content=df), + TypeError) + + def test_file_store1(self): + temp = get_temp_folder(__file__, "temp_file_storage") + name = os.path.join(temp, "filestore.db3") + SqlLite3FileStore(name) + SqlLite3FileStore(name) + + +if __name__ == "__main__": + unittest.main() diff --git a/_unittests/ut_serverdoc/test_file_store_rest.py b/_unittests/ut_serverdoc/test_file_store_rest.py new file mode 100644 index 00000000..e2f5e987 --- /dev/null +++ b/_unittests/ut_serverdoc/test_file_store_rest.py @@ -0,0 +1,39 @@ +""" +@brief test log(time=1s) + +""" +import unittest +import os +import pandas +from pyquickhelper.pycode import ExtTestCase, get_temp_folder +from pyquickhelper.server.filestore_fastapi import create_fast_api_app +from fastapi.testclient import TestClient # pylint: disable=E0401 + + +class TestfileStoreRest(ExtTestCase): + + def test_file_store(self): + temp = get_temp_folder(__file__, "temp_file_storage_rest") + name = os.path.join(temp, "filestore.db3") + app = create_fast_api_app(name, "BBB") + client = TestClient(app) + response = client.get("/") + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), + {'pyquickhelper': 'FastAPI to load and query files'}) + response = client.put( + "/add/", json=dict(name="essai", content="a,b\ne,0\nhh, 1.5", + password="CCC")) + self.assertEqual(response.status_code, 401) + response = client.put( + "/add/", json=dict(name="essai", content="a,b\ne,0\nhh, 1.5", + password="BBB")) + self.assertEqual(response.status_code, 200) + js = response.json() + self.assertIn('date', js) + self.assertNotIn('content', js) + + +if __name__ == "__main__": + unittest.main() diff --git a/requirements.txt b/requirements.txt index 0d5718a1..37352eda 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ codecov coverage>=5.0 cryptography docformatter +fastapi fire git-pandas gitdb diff --git a/setup.py b/setup.py index cb00507a..99d54ca1 100644 --- a/setup.py +++ b/setup.py @@ -266,6 +266,7 @@ def write_version(): 'keyrings.cryptfile'], 'jenkinshelper': ['python-jenkins>=1.0.0', 'pyyaml'], 'loghelper': ['psutil'], + 'server': ['fastapi'], 'all': [ "autopep8", # part of the minimal list 'cffi', @@ -274,6 +275,7 @@ def write_version(): "docformatter", "docutils", 'flake8', + 'fastapi', 'fire', "IPython>=5.0.0", "jupyter", diff --git a/src/pyquickhelper/ipythonhelper/magic_class_diff.py b/src/pyquickhelper/ipythonhelper/magic_class_diff.py index 957cb713..714bce52 100644 --- a/src/pyquickhelper/ipythonhelper/magic_class_diff.py +++ b/src/pyquickhelper/ipythonhelper/magic_class_diff.py @@ -77,7 +77,8 @@ def textdiff(self, line): if args is not None: html, js = create_visual_diff_through_html_files( args.f1, args.f2, encoding=args.encoding, notebook=True, - context_size=None if args.context in [None, ""] else int(args.context), + context_size=None if args.context in [ + None, ""] else int(args.context), inline_view=args.inline) display_html(html) return js diff --git a/src/pyquickhelper/server/filestore_fastapi.py b/src/pyquickhelper/server/filestore_fastapi.py new file mode 100644 index 00000000..18f8660e --- /dev/null +++ b/src/pyquickhelper/server/filestore_fastapi.py @@ -0,0 +1,48 @@ +# -*- coding:utf-8 -*- +""" +@file +@brief Simple class to store and retrieve files through an API. +""" +from typing import Optional +from fastapi import FastAPI, Request, HTTPException +from pydantic import BaseModel +from .filestore_sqlite import SqlLite3FileStore + + +class Item(BaseModel): + name: Optional[str] # pylint: disable=E1136 + format: Optional[str] # pylint: disable=E1136 + team: Optional[str] # pylint: disable=E1136 + project: Optional[str] # pylint: disable=E1136 + version: Optional[str] # pylint: disable=E1136 + content: Optional[str] # pylint: disable=E1136 + password: Optional[str] # pylint: disable=E1136 + + +def create_fast_api_app(db_path, password): + """ + Creates a :epkg:`REST` application based on :epkg:`FastAPI`. + + :return: app + """ + store = SqlLite3FileStore(db_path) + + async def get_root(): + return {"pyquickhelper": "FastAPI to load and query files"} + + async def add(item: Item, request: Request): + if item.password != password: + raise HTTPException(status_code=401, detail="Wrong password") + kwargs = dict(name=item.name, format=item.format, + team=item.team, project=item.project, + version=item.version, content=item.content) + kwargs['metadata'] = dict(client=request.client) + res = store.add(**kwargs) + if 'content' in res: + del res['content'] + return res + + app = FastAPI() + app.get("/")(get_root) + app.put("/add/")(add) + return app diff --git a/src/pyquickhelper/server/filestore_sqlite.py b/src/pyquickhelper/server/filestore_sqlite.py new file mode 100644 index 00000000..df873bbc --- /dev/null +++ b/src/pyquickhelper/server/filestore_sqlite.py @@ -0,0 +1,161 @@ +# -*- coding:utf-8 -*- +""" +@file +@brief Simple class to store and retrieve files with a sqlite3 detabase. +""" +import os +import io +import sqlite3 +import json +from datetime import datetime +import pandas + + +class SqlLite3FileStore: + """ + Simple file storage implemented with :epkg:`python:sqlite3`. + + :param path: location of the database. + """ + + def __init__(self, path="_file_store_.db3"): + self.path_ = path + self._create() + + def _create(self): + """ + Creates the database if it does not exists. + """ + self.con_ = sqlite3.connect(self.path_) + cur = self.con_.cursor() + cur.execute("SELECT name FROM sqlite_master WHERE type='table';") + res = cur.fetchall() + if ('files',) not in res: + cur.execute( + '''CREATE TABLE files + (id INTEGER PRIMARY KEY, date TEXT, name TEXT, + format TEXT, metadata TEXT, team TEXT, + project TEXT, version TEXT, content BLOB)''') + self.con_.commit() + + def add(self, name, content, format=None, date=None, metadata=None, + team=None, project=None, version=None): + """ + Adds a file to the database. + + :param name: filename + :param content: file content (it can be a dataframe) + :param format: format + :param date: date, by default now + :param metadata: addition information + :param team: another name + :param project: another name + :param version: version + """ + if date is None: + date = datetime.now() + date = date.isoformat() + if isinstance(metadata, dict): + metadata = json.dumps(metadata) + elif metadata is not None: + raise TypeError( + "metadata must be None or a dictionary.") + if isinstance(content, pandas.DataFrame): + st = io.StringIO() + content.to_csv(st, index=False, encoding="utf-8") + content = st.getvalue() + if format is None: + format = "df" + if format is None: + format = os.path.splitext(name)[-1] + record = dict(name=name, content=content, format=format, + metadata=metadata, team=team, project=project, + version=version, date=date) + fields = [] + values = [] + for k, n in record.items(): + if n is None: + continue + fields.append(k) + values.append(n.replace("\\", "\\\\").replace("'", "''")) + sqlite_insert_blob_query = """ + INSERT INTO files (%s) VALUES (%s)""" % ( + ",".join(fields), ",".join("'%s'" % v for v in values)) + cur = self.con_.cursor() + cur.execute(sqlite_insert_blob_query) + self.con_.commit() + output = dict(name=name, format=format, + metadata=metadata, team=team, project=project, + version=version, date=date) + return {k: v for k, v in output.items() if v is not None} + + def _enumerate(self, condition, fields): + cur = self.con_.cursor() + query = '''SELECT %s FROM files WHERE %s''' % ( + ",".join(fields), " AND ".join(condition)) + res = cur.execute(query) + + for line in res: + res = {k: v for k, v in zip(fields, line)} # pylint: disable=R1721 + if 'format' in res and 'content' in res and res['format'] == 'df': + st = io.StringIO(res['content']) + df = pandas.read_csv(st, encoding="utf-8") + res['content'] = df + if 'metadata' in res and res['metadata']: + res['metadata'] = json.loads(res['metadata']) + yield res + + def enumerate_content(self, name=None, format=None, date=None, metadata=None, + team=None, project=None, version=None): + """ + Queries the database, enumerates the results, + returns the content as well. + + :param name: filename + :param format: format + :param date: date, by default now + :param metadata: addition information + :param team: another name + :param project: another name + :param version: version + :return: results + """ + record = dict(name=name, format=format, + metadata=metadata, team=team, project=project, + version=version, date=date) + cond = [] + for k, v in record.items(): + if v is None: + continue + cond.append('%s="%s"' % (k, v)) + fields = ["id", "name", "format", "date", "metadata", + "team", "project", "version", "content"] + for it in self._enumerate(cond, fields): + yield it + + def enumerate(self, name=None, format=None, date=None, metadata=None, + team=None, project=None, version=None): + """ + Queries the database, enumerates the results. + + :param name: filename + :param format: format + :param date: date, by default now + :param metadata: addition information + :param team: another name + :param project: another name + :param version: version + :return: results + """ + record = dict(name=name, format=format, + metadata=metadata, team=team, project=project, + version=version, date=date) + cond = [] + for k, v in record.items(): + if v is None: + continue + cond.append('%s="%s"' % (k, v)) + fields = ["id", "name", "format", "date", "metadata", + "team", "project", "version"] + for it in self._enumerate(cond, fields): + yield it From 7b208062526e670d2ea541a0773a517dc4239e20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xavier=20dupr=C3=A9?= Date: Mon, 5 Apr 2021 19:56:47 +0200 Subject: [PATCH 2/3] add more functionalities --- _unittests/ut_serverdoc/test_file_store.py | 17 +++++ .../ut_serverdoc/test_file_store_rest.py | 15 +++- src/pyquickhelper/server/filestore_fastapi.py | 16 +++- src/pyquickhelper/server/filestore_sqlite.py | 76 +++++++++++++++++++ 4 files changed, 120 insertions(+), 4 deletions(-) diff --git a/_unittests/ut_serverdoc/test_file_store.py b/_unittests/ut_serverdoc/test_file_store.py index 9749200f..b8e6c605 100644 --- a/_unittests/ut_serverdoc/test_file_store.py +++ b/_unittests/ut_serverdoc/test_file_store.py @@ -32,6 +32,23 @@ def test_file_store(self): got = list(store.enumerate(name="zoo")) self.assertEqual(len(got), 1) + # data + idfile = record['id'] + store.add_data(idfile=idfile, name="ZOO", value="5.6") + res = list(store.enumerate_data(idfile)) + self.assertEqual(len(res), 1) + del res[0]['date'] + self.assertEqual(res, [{'id': 1, 'idfile': 1, 'name': 'ZOO', + 'value': 5.6}]) + + # data join + res = list(store.enumerate_data(idfile, join=True)) + self.assertEqual(len(res), 1) + del res[0]['date'] + self.assertEqual( + res, [{'id': 1, 'idfile': 1, 'name': 'ZOO', + 'name_f': 'zoo', 'value': 5.6}]) + def test_file_store_exc(self): temp = get_temp_folder(__file__, "temp_file_storage_exc") name = os.path.join(temp, "filestore.db3") diff --git a/_unittests/ut_serverdoc/test_file_store_rest.py b/_unittests/ut_serverdoc/test_file_store_rest.py index e2f5e987..c1124805 100644 --- a/_unittests/ut_serverdoc/test_file_store_rest.py +++ b/_unittests/ut_serverdoc/test_file_store_rest.py @@ -22,11 +22,12 @@ def test_file_store(self): self.assertEqual( response.json(), {'pyquickhelper': 'FastAPI to load and query files'}) - response = client.put( + response = client.post( "/add/", json=dict(name="essai", content="a,b\ne,0\nhh, 1.5", password="CCC")) self.assertEqual(response.status_code, 401) - response = client.put( + + response = client.post( "/add/", json=dict(name="essai", content="a,b\ne,0\nhh, 1.5", password="BBB")) self.assertEqual(response.status_code, 200) @@ -34,6 +35,16 @@ def test_file_store(self): self.assertIn('date', js) self.assertNotIn('content', js) + response = client.post( + "/query/", json=dict(name="essai", password="CCC")) + self.assertEqual(response.status_code, 401) + + response = client.post( + "/query/", json=dict(name="essai", password="BBB")) + self.assertEqual(response.status_code, 200) + js = response.json() + self.assertEqual(len(js), 0) + if __name__ == "__main__": unittest.main() diff --git a/src/pyquickhelper/server/filestore_fastapi.py b/src/pyquickhelper/server/filestore_fastapi.py index 18f8660e..bbd610e2 100644 --- a/src/pyquickhelper/server/filestore_fastapi.py +++ b/src/pyquickhelper/server/filestore_fastapi.py @@ -16,7 +16,12 @@ class Item(BaseModel): project: Optional[str] # pylint: disable=E1136 version: Optional[str] # pylint: disable=E1136 content: Optional[str] # pylint: disable=E1136 - password: Optional[str] # pylint: disable=E1136 + password: str + + +class Query(BaseModel): + name: str + password: str def create_fast_api_app(db_path, password): @@ -42,7 +47,14 @@ async def add(item: Item, request: Request): del res['content'] return res + async def query(query: Query, request: Request): + if query.password != password: + raise HTTPException(status_code=401, detail="Wrong password") + res = list(store.enumerate_data(name=query.name, join=True)) + return res + app = FastAPI() app.get("/")(get_root) - app.put("/add/")(add) + app.post("/add/")(add) + app.post("/query/")(query) return app diff --git a/src/pyquickhelper/server/filestore_sqlite.py b/src/pyquickhelper/server/filestore_sqlite.py index df873bbc..f45f1955 100644 --- a/src/pyquickhelper/server/filestore_sqlite.py +++ b/src/pyquickhelper/server/filestore_sqlite.py @@ -30,12 +30,21 @@ def _create(self): cur = self.con_.cursor() cur.execute("SELECT name FROM sqlite_master WHERE type='table';") res = cur.fetchall() + commit = False if ('files',) not in res: cur.execute( '''CREATE TABLE files (id INTEGER PRIMARY KEY, date TEXT, name TEXT, format TEXT, metadata TEXT, team TEXT, project TEXT, version TEXT, content BLOB)''') + commit = True + if ('files',) not in res: + cur.execute( + '''CREATE TABLE data + (id INTEGER PRIMARY KEY, idfile INTEGER, + name TEXT, value REAL, date TEXT)''') + commit = True + if commit: self.con_.commit() def add(self, name, content, format=None, date=None, metadata=None, @@ -51,6 +60,7 @@ def add(self, name, content, format=None, date=None, metadata=None, :param team: another name :param project: another name :param version: version + :return: added data as a dictionary (no content) """ if date is None: date = datetime.now() @@ -89,6 +99,29 @@ def add(self, name, content, format=None, date=None, metadata=None, version=version, date=date) return {k: v for k, v in output.items() if v is not None} + def add_data(self, idfile, name, value, date=None): + """ + Adds a file to the database. + + :param idfile: refers to database files + :param date: date, by default now + :param name: name + :param value: data value + :return: added data + """ + if date is None: + date = datetime.now() + date = date.isoformat() + + fields = ['idfile', 'date', 'name', 'value'] + values = [idfile, date, name, value] + sqlite_insert_blob_query = """ + INSERT INTO data (%s) VALUES (%s)""" % ( + ",".join(fields), ",".join("%r" % v for v in values)) + cur = self.con_.cursor() + cur.execute(sqlite_insert_blob_query) + self.con_.commit() + def _enumerate(self, condition, fields): cur = self.con_.cursor() query = '''SELECT %s FROM files WHERE %s''' % ( @@ -159,3 +192,46 @@ def enumerate(self, name=None, format=None, date=None, metadata=None, "team", "project", "version"] for it in self._enumerate(cond, fields): yield it + + def enumerate_data(self, idfile=None, name=None, join=False): + """ + Queries the database, enumerates the results. + + :param idfile: file identifier + :param name: value name, None if not specified + :param join: join with the table *files* + :return: results + """ + record = dict(name=name, idfile=idfile) + cond = [] + for k, v in record.items(): + if v is None: + continue + if join: + cond.append('data.%s="%s"' % (k, v)) + else: + cond.append('%s="%s"' % (k, v)) + cur = self.con_.cursor() + if join: + fields = ["data.id", "idfile", "data.name", "data.date", "value"] + fields2 = ['name', 'project', 'team', 'version'] + query = ''' + SELECT %s, %s + FROM data INNER JOIN files AS B on B.id = idfile + WHERE %s''' % ( + ",".join(fields), + ",".join(map(lambda s: "B.%s" % s, fields2)), + " AND ".join(cond)) + else: + fields = ["id", "idfile", "name", "date", "value"] + query = '''SELECT %s FROM data WHERE %s''' % ( + ",".join(fields), " AND ".join(cond)) + res = cur.execute(query) + + if join: + fields = ([s.replace('data.', '') for s in fields] + + ['name_f', 'project', 'team', 'version']) + for line in res: + res = {k: v for k, v in zip(fields, line) # pylint: disable=R1721 + if v is not None} + yield res From 81fb01210ad9d9df892d56ed3a3191175dc9451d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?xavier=20dupr=C3=A9?= Date: Tue, 6 Apr 2021 00:58:04 +0200 Subject: [PATCH 3/3] add query, metric --- .../ut_serverdoc/test_file_store_rest.py | 10 ++++ src/pyquickhelper/__main__.py | 5 +- src/pyquickhelper/cli/uvicorn_cli.py | 31 ++++++++++ src/pyquickhelper/helpgen/default_conf.py | 3 + src/pyquickhelper/server/filestore_fastapi.py | 60 ++++++++++++++++++- 5 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 src/pyquickhelper/cli/uvicorn_cli.py diff --git a/_unittests/ut_serverdoc/test_file_store_rest.py b/_unittests/ut_serverdoc/test_file_store_rest.py index c1124805..1154dc55 100644 --- a/_unittests/ut_serverdoc/test_file_store_rest.py +++ b/_unittests/ut_serverdoc/test_file_store_rest.py @@ -43,6 +43,16 @@ def test_file_store(self): "/query/", json=dict(name="essai", password="BBB")) self.assertEqual(response.status_code, 200) js = response.json() + self.assertEqual(len(js), 1) + + response = client.post( + "/metrics/", json=dict(name="essai", password="CCC")) + self.assertEqual(response.status_code, 401) + + response = client.post( + "/metrics/", json=dict(name="essai", password="BBB")) + self.assertEqual(response.status_code, 200) + js = response.json() self.assertEqual(len(js), 0) diff --git a/src/pyquickhelper/__main__.py b/src/pyquickhelper/__main__.py index ecb15461..61325395 100644 --- a/src/pyquickhelper/__main__.py +++ b/src/pyquickhelper/__main__.py @@ -31,6 +31,7 @@ def main(args, fLOG=print): from .cli.notebook import run_notebook, convert_notebook from .loghelper import set_password from .filehelper.download_urls_helper import download_urls_in_folder_content + from .cli.uvicorn_cli import uvicorn_app except ImportError: # pragma: no cover from pyquickhelper.cli.pyq_sync_cli import pyq_sync from pyquickhelper.cli.encryption_file_cli import encrypt_file, decrypt_file @@ -47,6 +48,7 @@ def main(args, fLOG=print): from pyquickhelper.cli.notebook import run_notebook, convert_notebook from pyquickhelper.loghelper import set_password from pyquickhelper.filehelper.download_urls_helper import download_urls_in_folder_content + from pyquickhelper.cli.uvicorn_cli import uvicorn_app fcts = dict(synchronize_folder=pyq_sync, encrypt_file=encrypt_file, decrypt_file=decrypt_file, encrypt=encrypt, @@ -58,7 +60,8 @@ def main(args, fLOG=print): zoom_img=zoom_img, images2pdf=images2pdf, repeat_script=repeat_script, ftp_upload=ftp_upload, set_password=set_password, - download_urls_in_folder_content=download_urls_in_folder_content) + download_urls_in_folder_content=download_urls_in_folder_content, + uvicorn_app=uvicorn_app) return cli_main_helper(fcts, args=args, fLOG=fLOG) diff --git a/src/pyquickhelper/cli/uvicorn_cli.py b/src/pyquickhelper/cli/uvicorn_cli.py new file mode 100644 index 00000000..8da9e4e9 --- /dev/null +++ b/src/pyquickhelper/cli/uvicorn_cli.py @@ -0,0 +1,31 @@ +""" +@file +@brief Simplified function versions. +""" +import os + + +def uvicorn_app(path="dummy_db.db3", pwd="dummy", port=8798, host="127.0.0.1"): + """ + Runs a uvicorn application. It should be used for testing + not for production. Use ``host:post/redoc`` or + ``host:post/docs`` to get a web page in order to + submit files. + + :param path: filename for the databse + :param pwd: password + :param host: host + :param port: port + + .. cmdref:: + :title: Runs a uvicorn application + :cmd: -m pyquickhelper uvicorn_app --help + + Runs a uvicorn application. + """ + from ..server.filestore_fastapi import create_app # pylint: disable=W0611 + import uvicorn + os.environ['PYQUICKHELPER_FASTAPI_PWD'] = pwd + os.environ['PYQUICKHELPER_FASTAPI_PATH'] = path + uvicorn.run("pyquickhelper.server.filestore_fastapi:create_app", + host=host, port=port, log_level="info", factory=True) diff --git a/src/pyquickhelper/helpgen/default_conf.py b/src/pyquickhelper/helpgen/default_conf.py index 031a4149..604de57d 100644 --- a/src/pyquickhelper/helpgen/default_conf.py +++ b/src/pyquickhelper/helpgen/default_conf.py @@ -94,6 +94,7 @@ def get_epkg_dictionary(): 'django': 'https://www.djangoproject.com/', 'docutils': 'http://docutils.sourceforge.net/', 'dvipng': 'https://ctan.org/pkg/dvipng?lang=en', + 'FastAPI': 'https://fastapi.tiangolo.com/', 'format style': 'https://pyformat.info/>`_', 'FTP': 'https://en.wikipedia.org/wiki/File_Transfer_Protocol', 'getsitepackages': 'https://docs.python.org/3/library/site.html#site.getsitepackages', @@ -188,6 +189,7 @@ def get_epkg_dictionary(): 'Python': 'http://www.python.org/', 'python-jenkins': 'http://python-jenkins.readthedocs.org/en/latest/', 'pywin32': 'https://sourceforge.net/projects/pywin32/', + 'REST': 'https://en.wikipedia.org/wiki/Representational_state_transfer', 'reveal.js': 'https://github.com/hakimel/reveal.js/releases', 'rst': 'https://en.wikipedia.org/wiki/ReStructuredText', 'RST': 'https://en.wikipedia.org/wiki/ReStructuredText', @@ -218,6 +220,7 @@ def get_epkg_dictionary(): 'tornado': 'http://www.tornadoweb.org/en/stable/', 'TortoiseSVN': 'http://tortoisesvn.net/', 'travis': 'https://travis-ci.org/', + 'uvicorn': 'https://www.uvicorn.org/', 'vis.js': 'https://visjs.org/', 'viz.js': 'https://github.com/mdaines/viz.js/', 'Visual Studio Community Edition 2015': 'https://imagine.microsoft.com/en-us/Catalog/Product/101', diff --git a/src/pyquickhelper/server/filestore_fastapi.py b/src/pyquickhelper/server/filestore_fastapi.py index bbd610e2..1f4fbd19 100644 --- a/src/pyquickhelper/server/filestore_fastapi.py +++ b/src/pyquickhelper/server/filestore_fastapi.py @@ -3,9 +3,10 @@ @file @brief Simple class to store and retrieve files through an API. """ +import os from typing import Optional from fastapi import FastAPI, Request, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel # pylint: disable=E0611 from .filestore_sqlite import SqlLite3FileStore @@ -19,11 +20,19 @@ class Item(BaseModel): password: str -class Query(BaseModel): +class Metric(BaseModel): name: str password: str +class Query(BaseModel): + name: Optional[str] # pylint: disable=E1136 + team: Optional[str] # pylint: disable=E1136 + project: Optional[str] # pylint: disable=E1136 + version: Optional[str] # pylint: disable=E1136 + password: str + + def create_fast_api_app(db_path, password): """ Creates a :epkg:`REST` application based on :epkg:`FastAPI`. @@ -47,14 +56,59 @@ async def add(item: Item, request: Request): del res['content'] return res - async def query(query: Query, request: Request): + async def metrics(query: Metric, request: Request): if query.password != password: raise HTTPException(status_code=401, detail="Wrong password") res = list(store.enumerate_data(name=query.name, join=True)) return res + async def query(query: Query, request: Request): + if query.password != password: + raise HTTPException(status_code=401, detail="Wrong password") + res = list(store.enumerate(name=query.name, team=query.team, + project=query.project, version=query.version)) + return res + app = FastAPI() app.get("/")(get_root) app.post("/add/")(add) + app.post("/metrics/")(metrics) app.post("/query/")(query) return app + + +def create_app(): + """ + Creates an instance of application class returned + by @see fn create_fast_api_app. It checks that + environment variables ``PYQUICKHELPER_FASTAPI_PWD`` + and ``PYQUICKHELPER_FASTAPI_PATH`` are set up with + a password and a filename. Otherwise, the function + raised an exception. + + Inspired from the guidelines + `uvicorn/deployment `_, + `(2) `_. + Some command lines: + + :: + + uvicorn --factory pyquickhelper.server.filestore_fastapi:create_app --port 8798 + --ssl-keyfile=./key.pem --ssl-certfile=./cert.pem + gunicorn --keyfile=./key.pem --certfile=./cert.pem -k uvicorn.workers.UvicornWorker + --factory pyquickhelper.server.filestore_fastapi:create_app + + :: + + uvicorn.run("pyquickhelper.server.filestore_fastapi:create_app", + host="127.0.0.1", port=8798, log_level="info", factory=True) + """ + if "PYQUICKHELPER_FASTAPI_PWD" not in os.environ: + raise RuntimeError( + "Environment variable PYQUICKHELPER_FASTAPI_PWD is missing.") + if "PYQUICKHELPER_FASTAPI_PATH" not in os.environ: + raise RuntimeError( + "Environment variable PYQUICKHELPER_FASTAPI_PATH is missing.") + app = create_fast_api_app(os.environ['PYQUICKHELPER_FASTAPI_PATH'], + os.environ['PYQUICKHELPER_FASTAPI_PWD']) + return app