From 94017407db22a6f7fbc61e2fcaeb703a0902eda1 Mon Sep 17 00:00:00 2001 From: Thaza_Kun <61819672+Thaza-Kun@users.noreply.github.com> Date: Mon, 21 Nov 2022 17:19:16 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=93=9A=20DOCS:=20CLI=20info?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 54 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 96bc0ba..a9522c5 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,15 @@ serta satu perkataan boleh dipadankan ke banyak kata asing yang bergantung pada ## Tentang Aplikasi +Pembangunan aplikasi ini ada dua arah: + +1. [Samudra Sebagai Aplikasi Terminal](#Samudra-Sebagai-Aplikasi-Terminal) +2. [Samudra Sebagai Aplikasi Pelayan](#Samudra-Sebagai-Aplikasi-Pelayan) + +> *warning* +> Buat masa ini pembangunan sedang ditumpukan pada arah 'Samudra Sebagai Aplikasi Terminal'. +> Maka, kefungsiannya sebagai satu pelayan dijangka rosak akibat perubahan kod teras. + ### Prasyarat 1. [Python 3.8](https://www.python.org/) atau lebih tinggi @@ -36,7 +45,18 @@ serta satu perkataan boleh dipadankan ke banyak kata asing yang bergantung pada $ poetry install ``` -### Sediakan fail .env +### Samudra Sebagai Aplikasi terminal + +Tempat masuk aplikasi terminal diletakkan dalam `./samudra/main.py`. +Oleh itu, arahan di bawah akan berikan senarai arahan yang tersedia. + +```shell +$ poetry run python ./samudra/main.py --help +``` + +### Samudra Sebagai Aplikasi Pelayan + +#### Sediakan fail .env Sebelum boleh memulakan pelayan, fail `.env` perlu wujud untuk menyediakan nilai yang berlainan bagi setiap salinan aplikasi. Fail ini terletak dalam ROOT, iaitu tempat yang sama wujudnya `README.md` ini. @@ -68,7 +88,7 @@ DATABASE_OPTIONS = ACCESS_TOKEN_EXPIRE_MINUTES = ``` -### Mulakan pelayan +#### Mulakan pelayan Setelah selesai langkah pemasangan, mulakan pelayan menggunakan poetry (poetry digunakan bagi memastikan arahan dalam fail python dilaksanakan @@ -82,21 +102,7 @@ Jika anda menerima ralat mengenai nilai-nilai yang tidak tertakrif, rujuk bahagi Lihat sekiranya nilai-nilai yang tersenarai di situ sudah tertakrif belum. Sekiranya nilai ralat itu tidak disenaraikan, boleh failkan isu. -### Menyumbang kod - -1. Buat cabang baharu agar setiap perubahan tersebut tidak mengganggu cabang utama. Namakan cabang secara deskriptif ( - seperti menamakan ciri yang ingin ditambah). - ```shell - $ git checkout -b nama_cabang - ``` -2. Buat perubahan yang diinginkan. Pastikan `git commit -m "ringkaskan perubahan di sini"` untuk simpan perubahan. -3. Hantar cabang perubahan ke repo github. Gunakan `nama_cabang` yang sama dengan langkah pertama. - ```shell - $ git push --set-upstream origin nama_cabang - ``` -4. Hantar _pull request_ dan jelaskan perubahan yang telah dilakukan. - -## Penciptaan lemma / konsep +### Penciptaan lemma / konsep Bagi memudahkan penulisan konsep, samudra menggunakan pencatatan berconteng tapis contengan tersebut menjadi struktur yang bermakna. Contengan perlu diletakkan di hujung keterangan kalau tidak, akan dihantar `SyntaxError` kerana ada lebih @@ -144,6 +150,20 @@ akan menghasilkan } ``` +### Menyumbang kod + +1. Buat cabang baharu agar setiap perubahan tersebut tidak mengganggu cabang utama. Namakan cabang secara deskriptif ( + seperti menamakan ciri yang ingin ditambah). + ```shell + $ git checkout -b nama_cabang + ``` +2. Buat perubahan yang diinginkan. Pastikan `git commit -m "ringkaskan perubahan di sini"` untuk simpan perubahan. +3. Hantar cabang perubahan ke repo github. Gunakan `nama_cabang` yang sama dengan langkah pertama. + ```shell + $ git push --set-upstream origin nama_cabang + ``` +4. Hantar _pull request_ dan jelaskan perubahan yang telah dilakukan. + ## Ingin Menyumbang? - Bahagian frontent boleh ke repo [alserembani94/laman-samudra](https://github.com/alserembani94/laman-samudra/). From 6362a7fa2a1875e83054b537c14d71f0d69cd246 Mon Sep 17 00:00:00 2001 From: Shumayl <83513359+mshumayl@users.noreply.github.com> Date: Tue, 22 Nov 2022 20:55:52 +0800 Subject: [PATCH 2/5] FIX: Bind model to database for server app #20 #21 --- samudra/serve.py | 2 +- samudra/server/setup.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/samudra/serve.py b/samudra/serve.py index ef58858..b9ce5a4 100644 --- a/samudra/serve.py +++ b/samudra/serve.py @@ -37,7 +37,7 @@ def root() -> Dict[str, str]: if __name__ == "__main__": # TODO Bind to database - uvicorn.run("serve:app", port=8000, reload=True) + uvicorn.run("serve:app", port=8000, reload=False) # TODO: CLI # TODO: Share lemma via picture diff --git a/samudra/server/setup.py b/samudra/server/setup.py index 8b13789..ba8568e 100644 --- a/samudra/server/setup.py +++ b/samudra/server/setup.py @@ -1 +1,16 @@ +import logging +import peewee as pw + +from samudra.models import create_tables +from samudra.conf import get_database +from samudra.conf.database.options import DatabaseEngine +from conf.setup import settings + +# TODO Remove ENGINE config +ENGINE = settings.get("database").get("engine", None) +DATABASE_NAME = settings.get("database").get("name", "samudra") +PATH = '' + +SERVER_DATABASE: pw.Database = get_database(engine=DatabaseEngine[ENGINE], db_name=DATABASE_NAME, path=PATH, new=True) +create_tables(database=SERVER_DATABASE) \ No newline at end of file From 84f0242940921b2eda63733956b622ce8e7a6dcd Mon Sep 17 00:00:00 2001 From: Thaza_Kun <61819672+Thaza-Kun@users.noreply.github.com> Date: Fri, 25 Nov 2022 23:16:04 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E2=9A=99=20FIX:=20The=20role=20of=20path?= =?UTF-8?q?=20and=20new=20in=20create=5Fsqlite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- samudra/conf/database/core.py | 29 ++++++++++++++++++----------- samudra/server/setup.py | 10 +++++++--- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/samudra/conf/database/core.py b/samudra/conf/database/core.py index 0afd665..53e6079 100644 --- a/samudra/conf/database/core.py +++ b/samudra/conf/database/core.py @@ -77,9 +77,12 @@ def __getattr__(self, name): # The DB connection object # ? Perlu ke test? # TODO Add Test + base_path: Path = Path(path, folder) + full_path: Path = Path(base_path, db_file) if new: - base_path: Path = Path(path, folder) - full_path: Path = Path(base_path, db_file) + # If a new database is to be created, check if the given path is occupied. + # If not occupied, create the database. + # If occupied, raise FileExistsError try: base_path.mkdir(parents=True) except FileExistsError: @@ -91,7 +94,7 @@ def __getattr__(self, name): print(f"Populating empty folder `{base_path.resolve()}` with {db_file}") else: raise FileExistsError( - f"The path `{base_path.resolve()}` is already occupied with something else. Consider using other folder." + f"The path `{base_path.resolve()}` is already occupied with something else. Try passing `new=False` to access the database or consider creating new database in another folder." ) # Set up readme README = Path(base_path, "README.md") @@ -103,14 +106,18 @@ def __getattr__(self, name): "Created using [samudra](https://github.com/samudradev/samudra)", ] ) - else: - db_obj = get_database_info(name=db_file) - if db_obj is None: - return FileNotFoundError( - f"The database name {db_file} is not found. Perhaps it is not created yet. Pass the key `new=True` if that's the case" - ) - base_path: Path = Path(db_obj["path"], folder=db_file) - full_path: Path = Path(base_path, db_file) + # else: + # # Originally, this part was intended to get the path to database via its name in the local `~/.samudra/databases.toml` file when `new=False`. + # # However, that would mean that the `path` parameter is rendered meaningless unless `new=True`. + # # Perhaps this functionality should be outside the function with paths and folder parameters as its result + # # which will be passed to `get_database/get_sqlite` with explicit `new=False` + # db_obj = get_database_info(name=db_file) + # if db_obj is None: + # return FileNotFoundError( + # f"The database name {db_file} is not found. Perhaps it is not created yet. Pass the key `new=True` if that's the case" + # ) + # base_path: Path = Path(db_obj["path"], folder=db_file) + # full_path: Path = Path(base_path, db_file) return_db = pw.SqliteDatabase( full_path.resolve(), check_same_thread=False, diff --git a/samudra/server/setup.py b/samudra/server/setup.py index ba8568e..f12d275 100644 --- a/samudra/server/setup.py +++ b/samudra/server/setup.py @@ -10,7 +10,11 @@ # TODO Remove ENGINE config ENGINE = settings.get("database").get("engine", None) DATABASE_NAME = settings.get("database").get("name", "samudra") -PATH = '' +PATH = "" -SERVER_DATABASE: pw.Database = get_database(engine=DatabaseEngine[ENGINE], db_name=DATABASE_NAME, path=PATH, new=True) -create_tables(database=SERVER_DATABASE) \ No newline at end of file +# We should find a way to only pass `new=True` upon first initialization of the server +# But pass `new=False` when restarting the server OR when database already exists +SERVER_DATABASE: pw.Database = get_database( + engine=DatabaseEngine[ENGINE], db_name=DATABASE_NAME, path=PATH, new=False +) +create_tables(database=SERVER_DATABASE) From 877cf210b71841f6ed52ae1186eab966c6c496d6 Mon Sep 17 00:00:00 2001 From: Thaza_Kun <61819672+Thaza-Kun@users.noreply.github.com> Date: Sat, 3 Dec 2022 13:58:17 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E2=99=BB=20REFACTOR:=20Uses=20db=20proxy?= =?UTF-8?q?=20to=20dynamically=20define=20connection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- samudra/cli/database.py | 22 ++-- samudra/cli/golongan_kata.py | 4 +- samudra/cli/lemma.py | 5 +- samudra/conf/database/core.py | 175 +++++++++++++------------ samudra/conf/database/options.py | 1 - samudra/conf/local.py | 37 ++++-- samudra/conf/setup.py | 4 +- samudra/core/auth/pengguna.py | 3 +- samudra/core/crud/__init__.py | 6 +- samudra/main.py | 7 + samudra/models/__init__.py | 10 +- samudra/models/base.py | 5 +- samudra/models/core/cakupan.py | 4 +- samudra/models/core/kata_asing.py | 2 +- samudra/models/core/konsep.py | 6 +- samudra/schemas/__init__.py | 10 +- samudra/schemas/tables/cakupan.py | 5 +- samudra/schemas/tables/konsep.py | 5 +- samudra/schemas/tables/lemma.py | 4 +- samudra/schemas/tables/user.py | 5 +- samudra/serve.py | 3 - samudra/server/dependencies.py | 2 +- samudra/server/routes/auth.py | 4 +- samudra/server/routes/golongan_kata.py | 3 +- samudra/server/routes/lemmas.py | 2 +- samudra/server/setup.py | 8 +- 26 files changed, 178 insertions(+), 164 deletions(-) diff --git a/samudra/cli/database.py b/samudra/cli/database.py index 0bd7b7b..1e14aed 100644 --- a/samudra/cli/database.py +++ b/samudra/cli/database.py @@ -1,19 +1,19 @@ from pathlib import Path import typer -from typer import Typer from rich import print +from typer import Typer -from samudra.models import create_tables -from samudra.conf.local import save_database, get_databases_config +from samudra.conf.database.core import get_database, set_active_database from samudra.conf.database.options import DatabaseEngine -from samudra.conf.database.core import get_database +from samudra.conf.local import read_databases_list +from samudra.models import create_tables app = Typer() @app.command() -def create( +def new( name: str = typer.Argument(..., rich_help_panel="Name of database."), path: str = typer.Option(".", rich_help_panel="Path to store database (SQLite)"), experimental: bool = typer.Option( @@ -27,21 +27,27 @@ def create( ), ) -> None: """Creates a new database""" - database = get_database(db_name=name, engine=engine, path=path, new=True) + database = get_database(name=name, engine=engine, path=path, new=True) tables = create_tables( database=database, auth=False, experimental=experimental, ) - save_database(db_name=name, path=Path(path)) print( f"`samudra.db` has been created in {Path(path, name).resolve()} with the following tables:" ) [print("-", table) for table in tables] +@app.command() +def set(name: str = typer.Argument(..., rich_help_panel="Name of database")) -> None: + """Sets an active database""" + set_active_database(name=name) + print(f"The active database has been set to `{name}`") + + @app.command() def list(): """Lists available databases""" # TODO Print as tables - print(get_databases_config()["databases"]) + print(read_databases_list()["databases"]) diff --git a/samudra/cli/golongan_kata.py b/samudra/cli/golongan_kata.py index f3a6e3c..34e074a 100644 --- a/samudra/cli/golongan_kata.py +++ b/samudra/cli/golongan_kata.py @@ -1,7 +1,7 @@ import typer -from samudra.core import crud from samudra.conf.setup import access_database +from samudra.core import crud from samudra.models import bind_to_database from samudra.schemas import CreateGolonganKata @@ -19,8 +19,6 @@ def new( ), ): """Creates a new word class.""" - # TODO Only bind during instantiation - bind_to_database(database=access_database(local=True), auth=True, experimental=True) crud.create_golongan_kata( data=CreateGolonganKata(id=id, nama=nama, keterangan=keterangan) ) diff --git a/samudra/cli/lemma.py b/samudra/cli/lemma.py index 5b44cee..05d9ca5 100644 --- a/samudra/cli/lemma.py +++ b/samudra/cli/lemma.py @@ -1,9 +1,9 @@ -from typing import List, Dict +from typing import List import typer -from samudra.core import crud from samudra.conf.setup import access_database +from samudra.core import crud from samudra.models import bind_to_database app = typer.Typer() @@ -31,7 +31,6 @@ def new( False, rich_help_panel="Create a new lemma not caring if it already created" ), ): - bind_to_database(database=access_database(local=True), auth=True, experimental=True) if concept is None: crud.create_lemma(lemma=lemma, force=force) crud.create_konsep( diff --git a/samudra/conf/database/core.py b/samudra/conf/database/core.py index 53e6079..ec2db2c 100644 --- a/samudra/conf/database/core.py +++ b/samudra/conf/database/core.py @@ -1,127 +1,134 @@ -from contextvars import ContextVar import logging import os -from dataclasses import dataclass +from contextvars import ContextVar from pathlib import Path -from unicodedata import name import peewee as pw -from conf.local import get_database_info - -# ! Importing settings will create circular import -# from conf.setup import settings +from conf.local import ( + read_database_info, + write_config, + read_config, + append_database_list, +) +from models.base import database_proxy from samudra.conf.database.options import DatabaseEngine -# TODO: Enforce requirements per database engine +db_state_default = {"closed": None, "conn": None, "ctx": None, "transactions": None} +db_state = ContextVar("db_state", default=db_state_default.copy()) -# As settings -# ENGINE = settings.get("database").get("engine", None) -# DATABASE_NAME = settings.get("database").get("name", "samudra") +class SQLiteConnectionState(pw._ConnectionState): + """Defaults to make SQLite DB async-compatible (according to FastAPI/Pydantic)""" -db_state_default = {"closed": None, "conn": None, "ctx": None, "transactions": None} -db_state = ContextVar("db_state", default=db_state_default.copy()) + def __init__(self, **kwargs): + super().__setattr__("_state", db_state) + super().__init__(**kwargs) + def __setattr__(self, name, value): + self._state.get()[name] = value -def get_database(db_name: str, engine: DatabaseEngine, **kwargs) -> pw.Database: - """ - Returns the connection class based on the engine. - """ - if engine is None or engine not in DatabaseEngine.__members__.values(): - raise ValueError( - "Please specify database engine in conf.toml. You entered {}. Valid values are: \n - {}".format( - engine, "\n - ".join(DatabaseEngine.__members__.values()) - ) - ) - if engine == DatabaseEngine.SQLite: - return get_sqlite( - folder=db_name, path=kwargs.pop("path"), new=kwargs.pop("new"), **kwargs + def __getattr__(self, name): + return self._state.get()[name] + + +def set_active_database(name: str) -> None: + """Sets the database as the currently active database""" + # Check if the name is already registered in .samudra/databases.toml + db_obj = read_database_info(name=name) + if db_obj is None: + raise FileNotFoundError( + f"The database name `{name}` is not found. Perhaps it is not created yet." ) + # Write the info in .samudra/config.toml + write_config({"active": name}) + # TODO ? Write relevant variables into .env for server? + +def new_database(name: str, engine: DatabaseEngine, path: str) -> pw.Database: + """Create and register a SQLite database or just register a database if not SQLite""" + # ? Should this be a function parameters? DATABASE_HOST = os.getenv("DATABASE_HOST") DATABASE_PORT = int(os.getenv("DATABASE_PORT")) DATABASE_OPTIONS = os.getenv("DATABASE_OPTIONS") USERNAME = os.getenv("DATABASE_USERNAME") PASSWORD = os.getenv("DATABASE_PASSWORD") SSL_MODE = os.getenv("SSL_MODE") - + if engine is None or engine not in DatabaseEngine.__members__.values(): + raise ValueError( + "Invalid engine. You entered {}. Valid values are: \n - {}".format( + engine, "\n - ".join(DatabaseEngine.__members__.values()) + ) + ) + if engine == DatabaseEngine.SQLite: + return create_sqlite(name=name, path=Path(path)) if engine == DatabaseEngine.MySQL: - conn_str = f"mysql://{USERNAME}:{PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{db_name}?ssl-mode=REQUIRED" + conn_str = f"mysql://{USERNAME}:{PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{name}?ssl-mode=REQUIRED" return_db = pw.MySQLDatabase(conn_str) - logging.info(f"Connecting to {return_db.database} as {USERNAME}") - if engine == DatabaseEngine.CockroachDB: - from playhouse.cockroachdb import CockroachDatabase - - conn_str = f"postgresql://{USERNAME}:{PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{db_name}?sslmode=verify-full&options={DATABASE_OPTIONS}" - return_db = CockroachDatabase(conn_str) + append_database_list(name=name, path=conn_str, engine=engine) logging.info(f"Connecting to {return_db.database} as {USERNAME}") else: raise NotImplementedError("Invalid engine") - return return_db -def get_sqlite(folder: str, path: str, db_file: str = "samudra.db", new: bool = False): - # Defaults to make it async-compatible (according to FastAPI/Pydantic) - class PeeweeConnectionState(pw._ConnectionState): - def __init__(self, **kwargs): - super().__setattr__("_state", db_state) - super().__init__(**kwargs) - - def __setattr__(self, name, value): - self._state.get()[name] = value - - def __getattr__(self, name): - return self._state.get()[name] - - # The DB connection object - # ? Perlu ke test? - # TODO Add Test - base_path: Path = Path(path, folder) - full_path: Path = Path(base_path, db_file) - if new: - # If a new database is to be created, check if the given path is occupied. - # If not occupied, create the database. - # If occupied, raise FileExistsError - try: - base_path.mkdir(parents=True) - except FileExistsError: - if full_path in [*base_path.iterdir()]: - raise FileExistsError( - f"A samudra database already exists in {full_path.resolve()}" - ) - elif [*base_path.iterdir()] is [None]: - print(f"Populating empty folder `{base_path.resolve()}` with {db_file}") - else: - raise FileExistsError( - f"The path `{base_path.resolve()}` is already occupied with something else. Try passing `new=False` to access the database or consider creating new database in another folder." - ) +def get_active_database() -> pw.Database: + active_database_name = read_config(key="active") + if not active_database_name: + raise KeyError("No active database is defined") + return get_database(name=active_database_name) + + +def get_database(name: str) -> pw.Database: + """Returns the connection class based on the name.""" + info = read_database_info(name) + if info.get("engine") == DatabaseEngine.SQLite: + return_db = pw.SqliteDatabase(info.get("path")) + return_db._state = SQLiteConnectionState() + return return_db + if info.get("engine") == DatabaseEngine.MySQL: + return pw.MySQLDatabase(info.get("path")) + + +def create_sqlite( + name: str, path: Path, filename: str = "samudra.db", description: str = "" +) -> pw.SqliteDatabase: + base_path: Path = Path(path, name) + full_path: Path = Path(base_path, filename) + # Check if the given path is occupied. + # If not occupied, create the database. + # If occupied, raise FileExistsError. + try: + base_path.mkdir(parents=True) + except FileExistsError: + if full_path in [*base_path.iterdir()]: + raise FileExistsError( + f"A samudra database already exists in {full_path.resolve()}" + ) + elif [*base_path.iterdir()] is [None]: + print(f"Populating empty folder `{base_path.resolve()}` with {filename}") + else: + raise FileExistsError( + f"The path `{base_path.resolve()}` is already occupied with something else. Consider creating new database in another folder." + ) # Set up readme README = Path(base_path, "README.md") README.touch() with README.open(mode="w") as f: f.writelines( [ - f"# {folder.title()}\n", + f"# {name.title()}\n", "Created using [samudra](https://github.com/samudradev/samudra)", + "", + description, ] ) - # else: - # # Originally, this part was intended to get the path to database via its name in the local `~/.samudra/databases.toml` file when `new=False`. - # # However, that would mean that the `path` parameter is rendered meaningless unless `new=True`. - # # Perhaps this functionality should be outside the function with paths and folder parameters as its result - # # which will be passed to `get_database/get_sqlite` with explicit `new=False` - # db_obj = get_database_info(name=db_file) - # if db_obj is None: - # return FileNotFoundError( - # f"The database name {db_file} is not found. Perhaps it is not created yet. Pass the key `new=True` if that's the case" - # ) - # base_path: Path = Path(db_obj["path"], folder=db_file) - # full_path: Path = Path(base_path, db_file) return_db = pw.SqliteDatabase( full_path.resolve(), check_same_thread=False, ) - return_db._state = PeeweeConnectionState() + return_db._state = SQLiteConnectionState() + database_proxy.init(return_db) + database_proxy.create_tables() logging.info(f"Connecting to {return_db.database}") + append_database_list(name=name, path=full_path, engine="sqlite") return return_db diff --git a/samudra/conf/database/options.py b/samudra/conf/database/options.py index 5079805..233102a 100644 --- a/samudra/conf/database/options.py +++ b/samudra/conf/database/options.py @@ -4,4 +4,3 @@ class DatabaseEngine(str, enum.Enum): SQLite = "sqlite" MySQL = "mysql" - CockroachDB = "cockroachdb" diff --git a/samudra/conf/local.py b/samudra/conf/local.py index ff4b9fc..8b50641 100644 --- a/samudra/conf/local.py +++ b/samudra/conf/local.py @@ -1,29 +1,46 @@ from collections import defaultdict from pathlib import Path -from typing import List, Dict, Optional +from typing import Dict, Optional, Union, Any import pytomlpp as toml HOME: Path = Path("~") dotconfig = Path(HOME, ".samudra").expanduser() -db_dotconfig = Path(dotconfig, "databases.toml") +database_list_file = Path(dotconfig, "databases.toml") +config_file = Path(dotconfig, "config.toml") + +database_list_file.touch() +config_file.touch() if not dotconfig.exists(): dotconfig.mkdir() -def save_database(db_name: str, path: Path): +def append_database_list(name: str, path: Union[Path, str], engine: str): databases: Dict[Dict] = defaultdict(dict) - databases[db_name] = path.resolve().__str__() - with open(db_dotconfig, mode="a") as f: + databases[name] = {"path": path.resolve().__str__(), "engine": engine} + with open(database_list_file, mode="a") as f: f.write(toml.dumps({"databases": databases})) -def get_databases_config() -> dict: - with open(db_dotconfig, mode="r") as f: - return toml.load(f) +def read_databases_list() -> dict: + return toml.load(database_list_file) + + +def read_database_info(name: str) -> Optional[dict]: + return read_databases_list().get("databases").get(name, None) + + +def read_config(key: Optional[str] = None) -> Any: + configs = toml.load(config_file, mode="r") + if key: + return configs.get(key) + return configs -def get_database_info(name: str) -> Optional[dict]: - return get_databases_config().get("databases").get(name, None) +def write_config(content: Dict) -> None: + configs = read_config(key=None) + for key, val in zip(content.keys(), content.values()): + configs[key] = val + toml.dump(configs, config_file, mode="w") diff --git a/samudra/conf/setup.py b/samudra/conf/setup.py index 65f8046..c4cef1e 100644 --- a/samudra/conf/setup.py +++ b/samudra/conf/setup.py @@ -1,5 +1,3 @@ -import os - import peewee import pytomlpp as toml @@ -11,6 +9,6 @@ def access_database(local: bool = True, name: str = None) -> peewee.Database: if local: - return get_database(db_name=name, engine=DatabaseEngine.SQLite, new=False) + return get_database(name=name, engine=DatabaseEngine.SQLite, new=False) else: raise NotImplementedError("Only loca database is implemented") diff --git a/samudra/core/auth/pengguna.py b/samudra/core/auth/pengguna.py index c69823e..c918d0e 100644 --- a/samudra/core/auth/pengguna.py +++ b/samudra/core/auth/pengguna.py @@ -1,12 +1,11 @@ """Functions relating to the management of [`Pengguna`][samudra.models.auth.Pengguna] """ +from passlib.context import CryptContext from peewee import prefetch from samudra import models -from passlib.context import CryptContext - pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") diff --git a/samudra/core/crud/__init__.py b/samudra/core/crud/__init__.py index 1b4d245..a89da66 100644 --- a/samudra/core/crud/__init__.py +++ b/samudra/core/crud/__init__.py @@ -7,8 +7,8 @@ - [golongan_kata][samudra.core.crud.golongan_kata] """ -from .lemma import * -from .konsep import * from .cakupan import * -from .kata_asing import * from .golongan_kata import * +from .kata_asing import * +from .konsep import * +from .lemma import * diff --git a/samudra/main.py b/samudra/main.py index e150a1d..585ff81 100644 --- a/samudra/main.py +++ b/samudra/main.py @@ -1,5 +1,7 @@ from typer import Typer +from conf.database.core import get_active_database +from models.base import database_proxy from samudra.cli import database, lemma, golongan_kata app = Typer() @@ -10,4 +12,9 @@ ) if __name__ == "__main__": + try: + active = get_active_database() + database_proxy.initialize(active) + except KeyError: + pass app() diff --git a/samudra/models/__init__.py b/samudra/models/__init__.py index bcc773a..3c1d33b 100644 --- a/samudra/models/__init__.py +++ b/samudra/models/__init__.py @@ -19,14 +19,14 @@ import peewee +from .auth.pengguna import Pengguna, Keizinan from .base import BaseDataTable +from .core.cakupan import Cakupan, CakupanXKonsep +from .core.kata_asing import KataAsing, KataAsingXKonsep +from .core.konsep import Konsep, GolonganKata # Ordered by table hierarchy from .core.lemma import Lemma -from .core.konsep import Konsep, GolonganKata -from .core.cakupan import Cakupan, CakupanXKonsep -from .core.kata_asing import KataAsing, KataAsingXKonsep -from .auth.pengguna import Pengguna, Keizinan from .experimental.petikan import Petikan, PetikanXKonsep, SumberPetikan @@ -52,6 +52,8 @@ def create_tables( return database.get_tables() +# Binding to database at runtime is time-consuming +# TODO ? REMOVE this in favor of db proxy init def bind_to_database( database: peewee.Database, auth: bool = True, experimental: bool = False ): diff --git a/samudra/models/base.py b/samudra/models/base.py index fc34631..9178276 100644 --- a/samudra/models/base.py +++ b/samudra/models/base.py @@ -10,9 +10,11 @@ from collections import defaultdict from typing import List, Dict, Tuple, Type -import peewee import peewee as pw +# TODO ! Initialize database at runtime using this proxy +database_proxy = pw.DatabaseProxy() + class BaseDataTable(pw.Model): """The simplest type of data model. @@ -37,6 +39,7 @@ class BaseDataTable(pw.Model): class Meta: legacy_table_names = False + database = database_proxy class BaseRelationshipTable(BaseDataTable): diff --git a/samudra/models/core/cakupan.py b/samudra/models/core/cakupan.py index 1d2dc9d..f78cc43 100644 --- a/samudra/models/core/cakupan.py +++ b/samudra/models/core/cakupan.py @@ -1,7 +1,7 @@ -from peewee import BlobField, TextField, ForeignKeyField +from peewee import TextField, ForeignKeyField -from ..base import BaseDataTable, BaseAttachmentDataTable, BaseRelationshipTable from .konsep import Konsep +from ..base import BaseAttachmentDataTable, BaseRelationshipTable class Cakupan(BaseAttachmentDataTable): diff --git a/samudra/models/core/kata_asing.py b/samudra/models/core/kata_asing.py index d77f619..ae3148b 100644 --- a/samudra/models/core/kata_asing.py +++ b/samudra/models/core/kata_asing.py @@ -1,7 +1,7 @@ from peewee import ForeignKeyField, TextField -from ..base import BaseAttachmentDataTable, BaseRelationshipTable from .konsep import Konsep +from ..base import BaseAttachmentDataTable, BaseRelationshipTable class KataAsing(BaseAttachmentDataTable): diff --git a/samudra/models/core/konsep.py b/samudra/models/core/konsep.py index fdfc648..48b2d74 100644 --- a/samudra/models/core/konsep.py +++ b/samudra/models/core/konsep.py @@ -1,23 +1,19 @@ from typing import Dict, List from peewee import ( - AutoField, TextField, IntegerField, - TimestampField, - BlobField, ForeignKeyField, ModelSelect, CharField, ) +from .lemma import Lemma from ..base import ( BaseDataTable, BaseAttachmentDataTable, - BaseRelationshipTable, BaseStrictDataTable, ) -from .lemma import Lemma class GolonganKata(BaseStrictDataTable): diff --git a/samudra/schemas/__init__.py b/samudra/schemas/__init__.py index c9352da..b0702ad 100644 --- a/samudra/schemas/__init__.py +++ b/samudra/schemas/__init__.py @@ -6,15 +6,13 @@ As such, I put off documenting it now as I prioritize other docs. """ -# Ordered by table hierarchy -from samudra.schemas.tables.lemma import LemmaResponse +from samudra.schemas.input.annotated_text import AnnotatedText +from samudra.schemas.tables.golongan_kata import CreateGolonganKata from samudra.schemas.tables.konsep import ( KonsepResponseFromTables, KonsepResponseFromAnnotatedBody, ) -from samudra.schemas.input.annotated_text import AnnotatedText - +# Ordered by table hierarchy +from samudra.schemas.tables.lemma import LemmaResponse from samudra.schemas.tables.user import LogMasukResponse, DaftarResponse - -from samudra.schemas.tables.golongan_kata import CreateGolonganKata diff --git a/samudra/schemas/tables/cakupan.py b/samudra/schemas/tables/cakupan.py index 75856cb..f2e5e1d 100644 --- a/samudra/schemas/tables/cakupan.py +++ b/samudra/schemas/tables/cakupan.py @@ -1,9 +1,6 @@ from typing import Optional -import pydantic as pyd - -from samudra import models -from samudra.schemas.tables._helper import PeeweeGetterDict, ORMSchema +from samudra.schemas.tables._helper import ORMSchema class CakupanResponse(ORMSchema): diff --git a/samudra/schemas/tables/konsep.py b/samudra/schemas/tables/konsep.py index a86923b..f72da96 100644 --- a/samudra/schemas/tables/konsep.py +++ b/samudra/schemas/tables/konsep.py @@ -1,9 +1,6 @@ from typing import Optional, List -import pydantic as pyd - -from samudra import models -from samudra.schemas.tables._helper import PeeweeGetterDict, ORMSchema +from samudra.schemas.tables._helper import ORMSchema from samudra.schemas.tables.cakupan import AttachCakupanToResponse, CakupanResponse from samudra.schemas.tables.kata_asing import ( AttachKataAsingToResponse, diff --git a/samudra/schemas/tables/lemma.py b/samudra/schemas/tables/lemma.py index 15bc1c9..8dbb507 100644 --- a/samudra/schemas/tables/lemma.py +++ b/samudra/schemas/tables/lemma.py @@ -1,8 +1,6 @@ from typing import List -import pydantic as pyd - -from samudra.schemas.tables._helper import PeeweeGetterDict, ORMSchema +from samudra.schemas.tables._helper import ORMSchema from samudra.schemas.tables.konsep import KonsepResponseFromTables diff --git a/samudra/schemas/tables/user.py b/samudra/schemas/tables/user.py index 3424e73..c8f5dff 100644 --- a/samudra/schemas/tables/user.py +++ b/samudra/schemas/tables/user.py @@ -1,9 +1,6 @@ -from typing import List - import pydantic as pyd -from samudra.schemas.tables._helper import PeeweeGetterDict, ORMSchema -from samudra.schemas.tables.konsep import KonsepResponseFromTables +from samudra.schemas.tables._helper import ORMSchema class Token(pyd.BaseModel): diff --git a/samudra/serve.py b/samudra/serve.py index b9ce5a4..1806887 100644 --- a/samudra/serve.py +++ b/samudra/serve.py @@ -4,7 +4,6 @@ from typing import Dict import uvicorn - from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -15,7 +14,6 @@ app = FastAPI() -# TODO: Add more server endpoints! app.include_router(lemmas.router) app.include_router(auth.router) app.include_router(golongan_kata.router) @@ -36,7 +34,6 @@ def root() -> Dict[str, str]: if __name__ == "__main__": - # TODO Bind to database uvicorn.run("serve:app", port=8000, reload=False) # TODO: CLI diff --git a/samudra/server/dependencies.py b/samudra/server/dependencies.py index a1cb3fe..52dc618 100644 --- a/samudra/server/dependencies.py +++ b/samudra/server/dependencies.py @@ -1,8 +1,8 @@ from fastapi import Depends from fastapi.security import OAuth2PasswordBearer -from samudra.server.setup import SERVER_DATABASE from samudra.conf.database.core import db_state_default +from samudra.server.setup import SERVER_DATABASE async def reset_db_state() -> None: diff --git a/samudra/server/routes/auth.py b/samudra/server/routes/auth.py index e55abfc..787be6a 100644 --- a/samudra/server/routes/auth.py +++ b/samudra/server/routes/auth.py @@ -1,11 +1,11 @@ +from datetime import timedelta + from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from samudra import models, schemas from samudra.core import auth from samudra.server.dependencies import get_db -from datetime import timedelta - from samudra.server.tokens import ( ACCESS_TOKEN_EXPIRE_MINUTES, PenggunaCreateDTO, diff --git a/samudra/server/routes/golongan_kata.py b/samudra/server/routes/golongan_kata.py index 7e109b5..a74657b 100644 --- a/samudra/server/routes/golongan_kata.py +++ b/samudra/server/routes/golongan_kata.py @@ -1,7 +1,8 @@ from typing import Union + from fastapi import APIRouter, Depends, HTTPException -from samudra import models +from samudra import models from samudra import schemas from samudra.core import crud from samudra.server.dependencies import get_db, oauth2_scheme diff --git a/samudra/server/routes/lemmas.py b/samudra/server/routes/lemmas.py index f85e44f..a76bbd3 100644 --- a/samudra/server/routes/lemmas.py +++ b/samudra/server/routes/lemmas.py @@ -4,8 +4,8 @@ from samudra import models, schemas from samudra.core import crud -from samudra.server.dependencies import get_db from samudra.schemas.input.query_filter import QueryFilter +from samudra.server.dependencies import get_db from samudra.server.dependencies import oauth2_scheme router = APIRouter(prefix="/lemma", dependencies=[Depends(get_db)]) diff --git a/samudra/server/setup.py b/samudra/server/setup.py index f12d275..c442645 100644 --- a/samudra/server/setup.py +++ b/samudra/server/setup.py @@ -1,11 +1,9 @@ -import logging - import peewee as pw -from samudra.models import create_tables +from conf.setup import settings from samudra.conf import get_database from samudra.conf.database.options import DatabaseEngine -from conf.setup import settings +from samudra.models import create_tables # TODO Remove ENGINE config ENGINE = settings.get("database").get("engine", None) @@ -15,6 +13,6 @@ # We should find a way to only pass `new=True` upon first initialization of the server # But pass `new=False` when restarting the server OR when database already exists SERVER_DATABASE: pw.Database = get_database( - engine=DatabaseEngine[ENGINE], db_name=DATABASE_NAME, path=PATH, new=False + engine=DatabaseEngine[ENGINE], name=DATABASE_NAME, path=PATH, new=False ) create_tables(database=SERVER_DATABASE) From a703c1cf830dfa96a08b238c0c45d9997d5206e2 Mon Sep 17 00:00:00 2001 From: Thaza_Kun <61819672+Thaza-Kun@users.noreply.github.com> Date: Tue, 28 Mar 2023 22:06:01 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=9A=80=20BUMP:=20v0.8.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- samudra/conf/database/core.py | 6 +++--- samudra/conf/setup.py | 13 +++++++++++-- samudra/core/crud/cakupan.py | 4 ++-- samudra/core/stats/__init__.py | 5 +++++ samudra/main.py | 4 ++-- samudra/server/routes/lemmas.py | 2 +- samudra/server/setup.py | 2 +- 8 files changed, 26 insertions(+), 12 deletions(-) create mode 100644 samudra/core/stats/__init__.py diff --git a/pyproject.toml b/pyproject.toml index eb0d68f..d03c13d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "samudra" -version = "0.8.0" +version = "0.8.3" description = "" authors = ["Thaza_Kun <61819672+Thaza-Kun@users.noreply.github.com>"] diff --git a/samudra/conf/database/core.py b/samudra/conf/database/core.py index ec2db2c..d0234f4 100644 --- a/samudra/conf/database/core.py +++ b/samudra/conf/database/core.py @@ -5,13 +5,13 @@ import peewee as pw -from conf.local import ( +from samudra.conf.local import ( read_database_info, write_config, read_config, append_database_list, ) -from models.base import database_proxy +from samudra.models.base import database_proxy from samudra.conf.database.options import DatabaseEngine db_state_default = {"closed": None, "conn": None, "ctx": None, "transactions": None} @@ -82,7 +82,7 @@ def get_database(name: str) -> pw.Database: """Returns the connection class based on the name.""" info = read_database_info(name) if info.get("engine") == DatabaseEngine.SQLite: - return_db = pw.SqliteDatabase(info.get("path")) + return_db = pw.SqliteDatabase(info.get("path"), check_same_thread=False) return_db._state = SQLiteConnectionState() return return_db if info.get("engine") == DatabaseEngine.MySQL: diff --git a/samudra/conf/setup.py b/samudra/conf/setup.py index c4cef1e..6417b4a 100644 --- a/samudra/conf/setup.py +++ b/samudra/conf/setup.py @@ -2,13 +2,22 @@ import pytomlpp as toml from samudra.conf import get_database +from samudra.conf.database.core import get_active_database from samudra.conf.database.options import DatabaseEngine +from samudra.models.base import database_proxy -settings = toml.load("conf.toml") +try: + settings = toml.load("conf.toml") +except FileNotFoundError: + pass def access_database(local: bool = True, name: str = None) -> peewee.Database: if local: return get_database(name=name, engine=DatabaseEngine.SQLite, new=False) else: - raise NotImplementedError("Only loca database is implemented") + raise NotImplementedError("Only local database is implemented") + + +def bind_proxy_with_active_database() -> None: + database_proxy.initialize(get_active_database()) diff --git a/samudra/core/crud/cakupan.py b/samudra/core/crud/cakupan.py index a9c86ca..c1e99a1 100644 --- a/samudra/core/crud/cakupan.py +++ b/samudra/core/crud/cakupan.py @@ -29,5 +29,5 @@ def get_cakupan_by_id( ) -def delete_lemma(lemma: models.Cakupan) -> int: - return lemma.delete_instance(recursive=False) +def delete_cakupan(cakupan: models.Cakupan) -> int: + return cakupan.delete_instance(recursive=False) diff --git a/samudra/core/stats/__init__.py b/samudra/core/stats/__init__.py new file mode 100644 index 0000000..335a806 --- /dev/null +++ b/samudra/core/stats/__init__.py @@ -0,0 +1,5 @@ +from samudra import models + + +def count_lemma() -> int: + return len(models.Lemma.select(models.Lemma.id)) diff --git a/samudra/main.py b/samudra/main.py index 585ff81..1ecdf33 100644 --- a/samudra/main.py +++ b/samudra/main.py @@ -1,7 +1,7 @@ from typer import Typer -from conf.database.core import get_active_database -from models.base import database_proxy +from samudra.conf.database.core import get_active_database +from samudra.models.base import database_proxy from samudra.cli import database, lemma, golongan_kata app = Typer() diff --git a/samudra/server/routes/lemmas.py b/samudra/server/routes/lemmas.py index a76bbd3..193565e 100644 --- a/samudra/server/routes/lemmas.py +++ b/samudra/server/routes/lemmas.py @@ -102,4 +102,4 @@ def delete_lemma(_id: int, token: str = Depends(oauth2_scheme)) -> Dict[str, int Returns how many items are deleted. """ lemma = crud.get_lemma_by_id(_id)[0] - return {"deleted": crud.delete_lemma(lemma)} + return {"deleted": crud.delete_cakupan(lemma)} diff --git a/samudra/server/setup.py b/samudra/server/setup.py index c442645..b6f4f96 100644 --- a/samudra/server/setup.py +++ b/samudra/server/setup.py @@ -1,6 +1,6 @@ import peewee as pw -from conf.setup import settings +from samudra.conf.setup import settings from samudra.conf import get_database from samudra.conf.database.options import DatabaseEngine from samudra.models import create_tables