Skip to content

Commit

Permalink
Feature: metadata fields
Browse files Browse the repository at this point in the history
  • Loading branch information
pylover committed Aug 25, 2024
1 parent 18ed29d commit d1ae83a
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 15 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions .gitmodules

This file was deleted.

21 changes: 14 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion activate.sh
1 change: 0 additions & 1 deletion make
Submodule make deleted from 072e72
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@


dependencies = [
'yhttp >= 5.0.2, < 6',
'yhttp >= 5.2, < 6',
'yhttp-dbmanager >= 4, < 5',
'sqlalchemy >= 2.0.32',
]
Expand Down
66 changes: 66 additions & 0 deletions tests/test_metadata.py
Original file line number Diff line number Diff line change
@@ -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)
42 changes: 42 additions & 0 deletions yhttp/ext/sqlalchemy/metadata.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit d1ae83a

Please sign in to comment.