From d1ae83a740e26b03c8001397b328dbf9a1f24135 Mon Sep 17 00:00:00 2001 From: pylover Date: Sun, 25 Aug 2024 05:37:02 +0330 Subject: [PATCH] Feature: metadata fields --- .github/workflows/build.yml | 2 +- .github/workflows/deploy.yml | 2 +- .gitmodules | 3 -- Makefile | 21 ++++++---- activate.sh | 2 +- make | 1 - setup.py | 2 +- tests/test_metadata.py | 66 ++++++++++++++++++++++++++++++++ yhttp/ext/sqlalchemy/metadata.py | 42 ++++++++++++++++++++ 9 files changed, 126 insertions(+), 15 deletions(-) delete mode 100644 .gitmodules delete mode 160000 make create mode 100644 tests/test_metadata.py create mode 100644 yhttp/ext/sqlalchemy/metadata.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ed00283..779e4c9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,7 +41,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - git clone https://github.com/pylover/python-makelib.git make + sudo git clone https://github.com/pylover/python-makelib.git /usr/local/lib/python-makelib make install-common editable-install sudo cp `which bddcli-bootstrapper` /usr/local/bin - name: Lint diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index cf95e43..780c634 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -17,7 +17,7 @@ jobs: uses: actions/setup-python@v5 - name: Install dependencies run: | - git clone https://github.com/pylover/python-makelib.git make + sudo git clone https://github.com/pylover/python-makelib.git /usr/local/lib/python-makelib make install-common editable-install - name: Create distributions run: make dist diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 426e7b5..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "make"] - path = make - url = git@github.com:pylover/python-makelib.git diff --git a/Makefile b/Makefile index 739a9a2..bab8c2e 100644 --- a/Makefile +++ b/Makefile @@ -7,10 +7,17 @@ PYDEPS_COMMON += \ 'yhttp-dev >= 3.1.2' -include make/common.mk -include make/install.mk -include make/lint.mk -include make/dist.mk -include make/pypi.mk -include make/test.mk -include make/venv.mk +# Assert the python-makelib version +PYTHON_MAKELIB_VERSION_REQUIRED = 1.4.1 + + +# Ensure the python-makelib is installed +PYTHON_MAKELIB_PATH = /usr/local/lib/python-makelib +ifeq ("", "$(wildcard $(PYTHON_MAKELIB_PATH))") + MAKELIB_URL = https://github.com/pylover/python-makelib + $(error python-makelib is not installed. see "$(MAKELIB_URL)") +endif + + +# Include a proper bundle rule file. +include $(PYTHON_MAKELIB_PATH)/venv-lint-test-pypi.mk diff --git a/activate.sh b/activate.sh index 69e62ee..fe31a10 120000 --- a/activate.sh +++ b/activate.sh @@ -1 +1 @@ -make/activate.sh \ No newline at end of file +/usr/local/lib/python-makelib/activate.sh \ No newline at end of file diff --git a/make b/make deleted file mode 160000 index 072e72d..0000000 --- a/make +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 072e72d85a18cfc772ddad6411a6b2be1e79dc97 diff --git a/setup.py b/setup.py index 7db0534..35c6d98 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ dependencies = [ - 'yhttp >= 5.0.2, < 6', + 'yhttp >= 5.2, < 6', 'yhttp-dbmanager >= 4, < 5', 'sqlalchemy >= 2.0.32', ] diff --git a/tests/test_metadata.py b/tests/test_metadata.py new file mode 100644 index 0000000..238e606 --- /dev/null +++ b/tests/test_metadata.py @@ -0,0 +1,66 @@ +import sqlalchemy as sa +from sqlalchemy.orm import MappedColumn +from sqlalchemy.orm import DeclarativeBase, mapped_column +from bddrest import status, response, when, given + +from yhttp.core import json +from yhttp.ext import sqlalchemy as saext, dbmanager +from yhttp.ext.sqlalchemy import metadata as m + + +def test_metadata(Given, freshdb, app): + field_title = m.String('title', length=(1, 3)) + field_alias = m.String('alias', optional=True, default='OOF') + + class Base(DeclarativeBase): + pass + + class Foo(Base): + __tablename__ = 'foo' + + id = mapped_column(sa.Integer, primary_key=True) + title = field_title.column(nullable=False) + alias = field_alias.column() + + dbmanager.install(app) + saext.install(app, Base) + app.ready() + app.db.create_objects() + + @app.route() + @app.bodyguard((field_title, field_alias), strict=True) + @json + def post(req): + session = app.db.session + with session.begin(): + f = Foo(title=req.form['title'], alias=req.form['alias']) + session.add(f) + + return dict(id=f.id, title=f.title, alias=f.alias) + + with Given(verb='post', form=dict(title='foo', alias='FOO')): + assert status == 200 + assert response.json == dict(id=1, title='foo', alias='FOO') + + when(form=given - 'title') + assert status == '400 title: Required' + + when(form=given - 'alias') + assert status == 200 + assert response.json == dict(id=2, title='foo', alias='OOF') + + +def test_metadata_args(): + col = m.String('foo', length=(0, 20)).column() + assert isinstance(col, MappedColumn) + assert isinstance(col.column.type, sa.String) + assert col.column.type.length == 20 + + col = m.String('foo').column() + assert isinstance(col, MappedColumn) + assert isinstance(col.column.type, sa.String) + assert col.column.type.length is None + + col = m.Integer('foo').column() + assert isinstance(col, MappedColumn) + assert isinstance(col.column.type, sa.Integer) diff --git a/yhttp/ext/sqlalchemy/metadata.py b/yhttp/ext/sqlalchemy/metadata.py new file mode 100644 index 0000000..0318a1b --- /dev/null +++ b/yhttp/ext/sqlalchemy/metadata.py @@ -0,0 +1,42 @@ +import abc + +import sqlalchemy as sa +from sqlalchemy.orm import mapped_column as sa_mapped_column + +from yhttp.core import guard as yguard + + +class FieldMixin(metaclass=abc.ABCMeta): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @property + @abc.abstractmethod + def _satype(self): + raise NotImplementedError + + def column(self, *args, nullable=None, **kwargs): + if nullable is not None: + kwargs['nullable'] = nullable + else: + kwargs['nullable'] = self.optional + + return sa_mapped_column( + self._satype, + *args, + **kwargs + ) + + +class String(FieldMixin, yguard.String): + @property + def _satype(self): + if self.length: + return sa.String(self.length[1]) + return sa.String + + +class Integer(FieldMixin, yguard.Integer): + @property + def _satype(self): + return sa.Integer