diff --git a/.travis.yml b/.travis.yml index 0e8c34f09c..a53b7fce1a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ sudo: required language: python python: - - "3.6" + - "3.5" service: - docker @@ -77,6 +77,9 @@ jobs: - docker-compose --version - python3 -m pip install --requirement=travis/requirements.txt script: + - python3 -m compileall commons/c2cgeoportal_commons + - python3 -m compileall geoportal/c2cgeoportal_geoportal + - python3 -m compileall admin/c2cgeoportal_admin - docker build --tag camptocamp/geomapfish-build-dev:${MAJOR_VERSION} docker/build - docker tag camptocamp/geomapfish-build-dev:${MAJOR_VERSION} camptocamp/geomapfish-build-dev-travis:${TRAVIS_BUILD_NUMBER} - ./docker-run travis/empty-make.sh help diff --git a/Jenkinsfile b/Jenkinsfile index 984159d3e3..c71673cb3a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -50,6 +50,7 @@ dockerBuild { sh './docker-run make admin/c2cgeoportal_admin/locale/c2cgeoportal_admin.pot' // lint sh './docker-run make flake8' + sh './docker-run make mypy' sh './docker-run make git-attributes' sh './docker-run make quote' sh './docker-run make spell' diff --git a/Makefile b/Makefile index 76fd75b392..54aea54747 100644 --- a/Makefile +++ b/Makefile @@ -97,7 +97,7 @@ build: $(MAKO_FILES:.mako=) \ doc: $(BUILD_DIR)/sphinx.timestamp .PHONY: checks -checks: flake8 git-attributes quote spell yamllint +checks: flake8 mypy git-attributes quote spell yamllint .PHONY: clean clean: @@ -166,6 +166,17 @@ flake8: --copyright-min-file-size=1 \ --copyright-regexp="Copyright \(c\) ([0-9][0-9][0-9][0-9]-)?$(shell date +%Y), Camptocamp SA" +.PHONY: mypy +mypy: + MYPYPATH=/usr/local/lib/python3.6/site-packages:/c2cwsgiutils \ + mypy --disallow-untyped-defs --strict-optional --follow-imports skip \ + commons/c2cgeoportal_commons + # TODO: add --disallow-untyped-defs + MYPYPATH=/usr/local/lib/python3.6/site-packages/ \ + mypy --ignore-missing-imports --strict-optional --follow-imports skip \ + geoportal/c2cgeoportal_geoportal \ + admin/c2cgeoportal_admin \ + .PHONY: git-attributes git-attributes: git --no-pager diff --check `git log --oneline | tail -1 | cut --fields=1 --delimiter=' '` diff --git a/admin/acceptance_tests/conftest.py b/admin/acceptance_tests/conftest.py index ac6ab87d26..68f9418858 100644 --- a/admin/acceptance_tests/conftest.py +++ b/admin/acceptance_tests/conftest.py @@ -1,16 +1,48 @@ import pytest +import transaction from pyramid import testing + from pyramid.paster import bootstrap from pyramid.view import view_config from webtest import TestApp from wsgiref.simple_server import make_server import threading -from c2cgeoportal_commons.tests import ( # noqa: F401 - dbsession, - transact, - raise_db_error_on_query, -) +from c2cgeoportal_commons.scripts.initializedb import init_db +from c2cgeoportal_commons.models import get_engine, get_session_factory, get_tm_session, generate_mappers +from sqlalchemy.exc import DBAPIError + + +@pytest.fixture(scope='session') +@pytest.mark.usefixtures("settings") +def dbsession(settings): + generate_mappers(settings) + engine = get_engine(settings) + init_db(engine, force=True) + session_factory = get_session_factory(engine) + session = get_tm_session(session_factory, transaction.manager) + return session + + +@pytest.fixture(scope='function') +@pytest.mark.usefixtures("dbsession") +def transact(dbsession): + t = dbsession.begin_nested() + yield + t.rollback() + + +def raise_db_error(Table): + raise DBAPIError('this is a test !', None, None) + + +@pytest.fixture(scope='function') +@pytest.mark.usefixtures("dbsession") +def raise_db_error_on_query(dbsession): + query = dbsession.query + dbsession.query = raise_db_error + yield + dbsession.query = query @pytest.fixture(scope="session") diff --git a/commons/acceptance_tests/conftest.py b/commons/acceptance_tests/conftest.py index 68068d6e57..d8dd994d52 100644 --- a/commons/acceptance_tests/conftest.py +++ b/commons/acceptance_tests/conftest.py @@ -1,7 +1,27 @@ import pytest -from c2cgeoportal_commons.tests import dbsession # noqa: F401 -from c2cgeoportal_commons.tests import transact # noqa: F401 +import transaction +from c2cgeoportal_commons.scripts.initializedb import init_db +from c2cgeoportal_commons.models import get_engine, get_session_factory, get_tm_session, generate_mappers + + +@pytest.fixture(scope='session') +@pytest.mark.usefixtures("settings") +def dbsession(settings): + generate_mappers(settings) + engine = get_engine(settings) + init_db(engine, force=True) + session_factory = get_session_factory(engine) + session = get_tm_session(session_factory, transaction.manager) + return session + + +@pytest.fixture(scope='function') +@pytest.mark.usefixtures("dbsession") +def transact(dbsession): + t = dbsession.begin_nested() + yield + t.rollback() @pytest.fixture(scope='session') diff --git a/commons/c2cgeoportal_commons/__init__.py b/commons/c2cgeoportal_commons/__init__.py index 99c98f3ecc..3a31827667 100644 --- a/commons/c2cgeoportal_commons/__init__.py +++ b/commons/c2cgeoportal_commons/__init__.py @@ -1,4 +1,7 @@ """c2cgeoportal_commons package.""" +from typing import Optional # noqa, pylint: disable=unused-import + +from pyramid.config import Configurator from c2cgeoportal_commons.models import ( # noqa get_session_factory, @@ -8,11 +11,11 @@ init_dbsessions, ) -schema = None -srid = None +schema = None # type: Optional[str] +srid = None # type: Optional[str] -def includeme(config): +def includeme(config: Configurator) -> None: """ Initialize the model for a Pyramid app. Activate this setup using ``config.include('c2cgeoportal_admin.commons')``. diff --git a/commons/c2cgeoportal_commons/models/__init__.py b/commons/c2cgeoportal_commons/models/__init__.py index 4fcf7c73b8..c7092403f7 100644 --- a/commons/c2cgeoportal_commons/models/__init__.py +++ b/commons/c2cgeoportal_commons/models/__init__.py @@ -30,34 +30,41 @@ import re import logging +from typing import Optional, Dict, Any + +from c2cwsgiutils.health_check import HealthCheck +from pyramid.config import Configurator from sqlalchemy import engine_from_config import sqlalchemy.ext.declarative -from sqlalchemy.orm import sessionmaker, configure_mappers +from sqlalchemy.engine import Engine +from sqlalchemy.orm import sessionmaker, configure_mappers, Session +import sqlalchemy.ext.declarative.api import zope.sqlalchemy import c2cwsgiutils +from transaction import TransactionManager import c2cgeoportal_commons.models # "7/11/17, fix ci build, avoid use of globals" LOG = logging.getLogger(__name__) DBSession = None # Initialized by init_dbsessions -Base = sqlalchemy.ext.declarative.declarative_base() -DBSessions = {} +Base = sqlalchemy.ext.declarative.declarative_base() # type: sqlalchemy.ext.declarative.api.Base +DBSessions = {} # type: Dict[str, Session] -srid = None -schema = None +srid = None # type: Optional[str] +schema = None # type: Optional[str] -def get_engine(settings, prefix='sqlalchemy.'): +def get_engine(settings: dict, prefix: str='sqlalchemy.') -> Engine: return engine_from_config(settings, prefix) -def get_session_factory(engine): +def get_session_factory(engine: Engine) -> sessionmaker: factory = sessionmaker() factory.configure(bind=engine) return factory -def get_tm_session(session_factory, transaction_manager): +def get_tm_session(session_factory: sessionmaker, transaction_manager: TransactionManager) -> Session: """ Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. @@ -84,14 +91,14 @@ def get_tm_session(session_factory, transaction_manager): return dbsession -def generate_mappers(settings): +def generate_mappers(settings: dict) -> None: """ Initialize the model for a Pyramid app. """ global schema - schema = settings.get('schema') + schema = settings['schema'] global srid - srid = settings.get('srid') + srid = settings['srid'] # import or define all models here to ensure they are attached to the # Base.metadata prior to any initialization routines @@ -101,7 +108,7 @@ def generate_mappers(settings): # all relationships can be setup configure_mappers() -def init_dbsessions(settings, config=None, health_check=None): +def init_dbsessions(settings: dict, config: Configurator=None, health_check: HealthCheck=None) -> None: # define the srid, schema as global variables to be usable in the model global schema global srid @@ -123,6 +130,7 @@ def init_dbsessions(settings, config=None, health_check=None): c2cgeoportal_commons.models.DBSessions[dbsession_name] = \ c2cwsgiutils.db.create_session(config, dbsession_name, **dbsession_config) + Base.metadata.clear() from c2cgeoportal_commons.models import main if health_check is not None: diff --git a/commons/c2cgeoportal_commons/models/main.py b/commons/c2cgeoportal_commons/models/main.py index d06d6fa0da..e7c7e51ed2 100644 --- a/commons/c2cgeoportal_commons/models/main.py +++ b/commons/c2cgeoportal_commons/models/main.py @@ -29,6 +29,8 @@ import logging +from typing import List, Optional, Any +from typing import Union, Tuple, Dict # noqa, pylint: disable=unused-import from papyrus.geo_interface import GeoInterface from sqlalchemy import ForeignKey, Table, event @@ -55,7 +57,7 @@ from pyramid.i18n import TranslationStringFactory _ = TranslationStringFactory("c2cgeoportal") except ImportError: - def _(s): + def _(s: str) -> str: return s @@ -77,7 +79,7 @@ def _(s): ) -def cache_invalidate_cb(*args): +def cache_invalidate_cb(*args: List[Any]) -> None: # caching.invalidate_region() # we should probably use this debounce https://gist.github.com/esromneb/8eac6bf5bdfef58304cb @@ -88,7 +90,7 @@ def cache_invalidate_cb(*args): class TsVector(UserDefinedType): """ A custom type for PostgreSQL's tsvector type. """ - def get_col_spec(self): # pragma: no cover + def get_col_spec(self) -> str: # pragma: no cover return "TSVECTOR" @@ -128,12 +130,12 @@ class Functionality(Base): value = Column(Unicode, nullable=False) description = Column(Unicode) - def __init__(self, name="", value="", description=""): + def __init__(self, name: str="", value: str="", description: str="") -> None: self.name = name self.value = value self.description = description - def __unicode__(self): + def __unicode__(self) -> str: return "{0!s} - {1!s}".format(self.name or "", self.value or "") # pragma: no cover @@ -144,28 +146,16 @@ def __unicode__(self): # association table role <> functionality role_functionality = Table( "role_functionality", Base.metadata, - Column( - "role_id", Integer, - ForeignKey(_schema + ".role.id"), primary_key=True - ), - Column( - "functionality_id", Integer, - ForeignKey(_schema + ".functionality.id"), primary_key=True - ), + Column("role_id", Integer, ForeignKey(_schema + ".role.id"), primary_key=True), + Column("functionality_id", Integer, ForeignKey(_schema + ".functionality.id"), primary_key=True), schema=_schema ) # association table theme <> functionality theme_functionality = Table( "theme_functionality", Base.metadata, - Column( - "theme_id", Integer, - ForeignKey(_schema + ".theme.id"), primary_key=True - ), - Column( - "functionality_id", Integer, - ForeignKey(_schema + ".functionality.id"), primary_key=True - ), + Column("theme_id", Integer, ForeignKey(_schema + ".theme.id"), primary_key=True), + Column("functionality_id", Integer, ForeignKey(_schema + ".functionality.id"), primary_key=True), schema=_schema ) @@ -205,7 +195,10 @@ class Role(Base): cascade="save-update,merge,refresh-expire" ) - def __init__(self, name="", description="", functionalities=None, extent=None): + def __init__( + self, name: str="", description: str="", + functionalities: List[Functionality]=None, extent: Geometry=None + ) -> None: if functionalities is None: functionalities = [] self.name = name @@ -213,11 +206,11 @@ def __init__(self, name="", description="", functionalities=None, extent=None): self.extent = extent self.description = description - def __unicode__(self): + def __unicode__(self) -> str: return self.name or "" # pragma: no cover @property - def bounds(self): + def bounds(self) -> None: if self.extent is None: return None return to_shape(self.extent).bounds @@ -233,7 +226,7 @@ class TreeItem(Base): __table_args__ = ( UniqueConstraint("type", "name"), {"schema": _schema}, - ) + ) # type: Union[Tuple, Dict[str, Any]] __acl__ = [DENY_ALL] item_type = Column("type", String(10), nullable=False) __mapper_args__ = {"polymorphic_on": item_type} @@ -244,10 +237,11 @@ class TreeItem(Base): description = Column(Unicode) @property - def parents(self): # pragma: no cover + # Better: def parents(self) -> List[TreeGroup]: # pragma: no cover + def parents(self) -> List['TreeItem']: # pragma: no cover return [c.group for c in self.parents_relation] - def is_in_interface(self, name): + def is_in_interface(self, name: str) -> bool: if not hasattr(self, "interfaces"): # pragma: no cover return False @@ -257,10 +251,10 @@ def is_in_interface(self, name): return False - def get_metadatas(self, name): # pragma: no cover + def get_metadatas(self, name: str) -> List['Metadata']: # pragma: no cover return [metadata for metadata in self.metadatas if metadata.name == name] - def __init__(self, name=""): + def __init__(self, name: str="") -> None: self.name = name @@ -280,9 +274,7 @@ class LayergroupTreeitem(Base): # required by formalchemy id = Column(Integer, primary_key=True) description = Column(Unicode) - treegroup_id = Column( - Integer, ForeignKey(_schema + ".treegroup.id") - ) + treegroup_id = Column(Integer, ForeignKey(_schema + ".treegroup.id")) treegroup = relationship( "TreeGroup", backref=backref( @@ -292,9 +284,7 @@ class LayergroupTreeitem(Base): ), primaryjoin="LayergroupTreeitem.treegroup_id==TreeGroup.id", ) - treeitem_id = Column( - Integer, ForeignKey(_schema + ".treeitem.id") - ) + treeitem_id = Column(Integer, ForeignKey(_schema + ".treeitem.id")) treeitem = relationship( "TreeItem", backref=backref( @@ -304,7 +294,7 @@ class LayergroupTreeitem(Base): ) ordering = Column(Integer) - def __init__(self, group=None, item=None, ordering=0): + def __init__(self, group: 'TreeGroup'=None, item: TreeItem=None, ordering: int=0) -> None: self.treegroup = group self.treeitem = item self.ordering = ordering @@ -320,14 +310,12 @@ class TreeGroup(TreeItem): __table_args__ = {"schema": _schema} __acl__ = [DENY_ALL] - id = Column( - Integer, ForeignKey(_schema + ".treeitem.id"), primary_key=True - ) + id = Column(Integer, ForeignKey(_schema + ".treeitem.id"), primary_key=True) - def _get_children(self): + def _get_children(self) -> List[TreeItem]: return [c.treeitem for c in self.children_relation] - def _set_children(self, children, order=False): + def _set_children(self, children: List[TreeItem], order: bool=False) -> None: for child in self.children_relation: if child.treeitem not in children: child.treeitem = None @@ -350,7 +338,7 @@ def _set_children(self, children, order=False): children = property(_get_children, _set_children) - def __init__(self, name=""): + def __init__(self, name: str="") -> None: TreeItem.__init__(self, name=name) @@ -362,17 +350,15 @@ class LayerGroup(TreeGroup): ] __mapper_args__ = {"polymorphic_identity": "group"} - id = Column( - Integer, ForeignKey(_schema + ".treegroup.id"), primary_key=True - ) + id = Column(Integer, ForeignKey(_schema + ".treegroup.id"), primary_key=True) is_expanded = Column(Boolean) # shouldn"t be used in V3 is_internal_wms = Column(Boolean) # children have radio button instance of check box is_base_layer = Column(Boolean) # Should not be used in V3 def __init__( - self, name="", is_expanded=False, - is_internal_wms=True, is_base_layer=False): + self, name: str="", is_expanded: bool=False, is_internal_wms: bool=True, is_base_layer: bool=False + ) -> None: TreeGroup.__init__(self, name=name) self.is_expanded = is_expanded self.is_internal_wms = is_internal_wms @@ -382,13 +368,8 @@ def __init__( # role theme link for restricted theme restricted_role_theme = Table( "restricted_role_theme", Base.metadata, - Column( - "role_id", Integer, ForeignKey(_schema + ".role.id"), primary_key=True - ), - Column( - "theme_id", Integer, - ForeignKey(_schema + ".theme.id"), primary_key=True - ), + Column("role_id", Integer, ForeignKey(_schema + ".role.id"), primary_key=True), + Column("theme_id", Integer, ForeignKey(_schema + ".theme.id"), primary_key=True), schema=_schema ) @@ -401,9 +382,7 @@ class Theme(TreeGroup): ] __mapper_args__ = {"polymorphic_identity": "theme"} - id = Column( - Integer, ForeignKey(_schema + ".treegroup.id"), primary_key=True - ) + id = Column(Integer, ForeignKey(_schema + ".treegroup.id"), primary_key=True) ordering = Column(Integer, nullable=False) public = Column(Boolean, default=True, nullable=False) icon = Column(Unicode) @@ -420,7 +399,7 @@ class Theme(TreeGroup): cascade="save-update,merge,refresh-expire", ) - def __init__(self, name="", ordering=100, icon=""): + def __init__(self, name: str="", ordering: int=100, icon: str="") -> None: TreeGroup.__init__(self, name=name) self.ordering = ordering self.icon = icon @@ -436,14 +415,12 @@ class Layer(TreeItem): __table_args__ = {"schema": _schema} __acl__ = [DENY_ALL] - id = Column( - Integer, ForeignKey(_schema + ".treeitem.id"), primary_key=True - ) + id = Column(Integer, ForeignKey(_schema + ".treeitem.id"), primary_key=True) public = Column(Boolean, default=True) geo_table = Column(Unicode) exclude_properties = Column(Unicode) - def __init__(self, name="", public=True): + def __init__(self, name: str="", public: bool=True) -> None: TreeItem.__init__(self, name=name) self.public = public @@ -467,15 +444,11 @@ class LayerV1(Layer): # Deprecated in v2 is_checked = Column(Boolean, default=True) # by default icon = Column(Unicode) # on the tree layer_type = Column(Enum( - "internal WMS", - "external WMS", - "WMTS", - "no 2D", + "internal WMS", "external WMS", "WMTS", "no 2D", native_enum=False)) url = Column(Unicode) # for externals image_type = Column(Enum( - "image/jpeg", - "image/png", + "image/jpeg", "image/png", native_enum=False)) # for WMS style = Column(Unicode) dimensions = Column(Unicode) # for WMTS @@ -495,21 +468,18 @@ class LayerV1(Layer): # Deprecated in v2 # data attribute field in which application can find a human identifiable name or number identifier_attribute_field = Column(Unicode) time_mode = Column(Enum( - "disabled", - "value", - "range", + "disabled", "value", "range", native_enum=False), default="disabled", nullable=False, ) time_widget = Column(Enum( - "slider", - "datepicker", + "slider", "datepicker", native_enum=False), default="slider", nullable=True, ) def __init__( - self, name="", public=True, icon="", - layer_type="internal WMS" - ): + self, name: str="", public: bool=True, icon: str="", + layer_type: str="internal WMS" + ) -> None: Layer.__init__(self, name=name, public=public) self.layer = name self.icon = icon @@ -534,9 +504,7 @@ class OGCServer(Base): (Allow, AUTHORIZED_ROLE, ALL_PERMISSIONS), ] - id = Column( - Integer, primary_key=True - ) + id = Column(Integer, primary_key=True) name = Column(Unicode, nullable=False, unique=True) description = Column(Unicode) url = Column(Unicode, nullable=False) @@ -561,10 +529,10 @@ class OGCServer(Base): is_single_tile = Column(Boolean) def __init__( - self, name="", description=None, url="https://wms.example.com", url_wfs=None, - type_="mapserver", image_type="image/png", auth="Standard auth", wfs_support=True, - is_single_tile=False - ): + self, name: str="", description: Optional[str]=None, url: str="https://wms.example.com", + url_wfs: str=None, type_: str="mapserver", image_type: str="image/png", auth: str="Standard auth", + wfs_support: bool=True, is_single_tile: bool=False + ) -> None: self.name = name self.description = description self.url = url @@ -575,7 +543,7 @@ def __init__( self.wfs_support = wfs_support self.is_single_tile = is_single_tile - def __unicode__(self): + def __unicode__(self) -> str: return self.name or "" # pragma: no cover @@ -587,23 +555,16 @@ class LayerWMS(DimensionLayer): ] __mapper_args__ = {"polymorphic_identity": "l_wms"} - id = Column( - Integer, ForeignKey(_schema + ".layer.id"), primary_key=True - ) - ogc_server_id = Column( - Integer, ForeignKey(_schema + ".ogc_server.id"), nullable=False - ) + id = Column(Integer, ForeignKey(_schema + ".layer.id"), primary_key=True) + ogc_server_id = Column(Integer, ForeignKey(_schema + ".ogc_server.id"), nullable=False) layer = Column(Unicode) style = Column(Unicode) time_mode = Column(Enum( - "disabled", - "value", - "range", + "disabled", "value", "range", native_enum=False), default="disabled", nullable=False, ) time_widget = Column(Enum( - "slider", - "datepicker", + "slider", "datepicker", native_enum=False), default="slider", nullable=False, ) @@ -613,8 +574,10 @@ class LayerWMS(DimensionLayer): ) def __init__( - self, name="", layer="", public=True, time_mode="disabled", time_widget="slider" - ): + self, name: str="", layer: str="", public: bool=True, + time_mode: str="disabled", + time_widget: str="slider" + ) -> None: DimensionLayer.__init__(self, name=name, public=public) self.layer = layer self.time_mode = time_mode @@ -629,9 +592,7 @@ class LayerWMTS(DimensionLayer): ] __mapper_args__ = {"polymorphic_identity": "l_wmts"} - id = Column( - Integer, ForeignKey(_schema + ".layer.id"), primary_key=True - ) + id = Column(Integer, ForeignKey(_schema + ".layer.id"), primary_key=True) url = Column(Unicode, nullable=False) layer = Column(Unicode, nullable=False) style = Column(Unicode) @@ -642,7 +603,7 @@ class LayerWMTS(DimensionLayer): native_enum=False), nullable=False ) - def __init__(self, name="", public=True, image_type="image/png"): + def __init__(self, name: str="", public: bool=True, image_type: str="image/png") -> None: DimensionLayer.__init__(self, name=name, public=public) self.image_type = image_type @@ -650,27 +611,16 @@ def __init__(self, name="", public=True, image_type="image/png"): # association table role <> restriciton area role_ra = Table( "role_restrictionarea", Base.metadata, - Column( - "role_id", Integer, ForeignKey(_schema + ".role.id"), primary_key=True - ), - Column( - "restrictionarea_id", Integer, - ForeignKey(_schema + ".restrictionarea.id"), primary_key=True - ), + Column("role_id", Integer, ForeignKey(_schema + ".role.id"), primary_key=True), + Column("restrictionarea_id", Integer, ForeignKey(_schema + ".restrictionarea.id"), primary_key=True), schema=_schema ) # association table layer <> restriciton area layer_ra = Table( "layer_restrictionarea", Base.metadata, - Column( - "layer_id", Integer, - ForeignKey(_schema + ".layer.id"), primary_key=True - ), - Column( - "restrictionarea_id", Integer, - ForeignKey(_schema + ".restrictionarea.id"), primary_key=True - ), + Column("layer_id", Integer, ForeignKey(_schema + ".layer.id"), primary_key=True), + Column("restrictionarea_id", Integer, ForeignKey(_schema + ".restrictionarea.id"), primary_key=True), schema=_schema ) @@ -702,8 +652,9 @@ class RestrictionArea(Base): backref="restrictionareas", cascade="save-update,merge,refresh-expire" ) - def __init__(self, name="", description="", layers=None, roles=None, - area=None, readwrite=False): + def __init__( + self, name: str="", description: str="", layers: List[Layer]=None, roles: List[Role]=None, + area: Geometry=None, readwrite: bool=False) -> None: if layers is None: layers = [] if roles is None: @@ -715,7 +666,7 @@ def __init__(self, name="", description="", layers=None, roles=None, self.area = area self.readwrite = readwrite - def __unicode__(self): # pragma: no cover + def __unicode__(self) -> str: # pragma: no cover return self.name or "" @@ -727,28 +678,16 @@ def __unicode__(self): # pragma: no cover # association table interface <> layer interface_layer = Table( "interface_layer", Base.metadata, - Column( - "interface_id", Integer, - ForeignKey(_schema + ".interface.id"), primary_key=True - ), - Column( - "layer_id", Integer, - ForeignKey(_schema + ".layer.id"), primary_key=True - ), + Column("interface_id", Integer, ForeignKey(_schema + ".interface.id"), primary_key=True), + Column("layer_id", Integer, ForeignKey(_schema + ".layer.id"), primary_key=True), schema=_schema ) # association table interface <> theme interface_theme = Table( "interface_theme", Base.metadata, - Column( - "interface_id", Integer, - ForeignKey(_schema + ".interface.id"), primary_key=True - ), - Column( - "theme_id", Integer, - ForeignKey(_schema + ".theme.id"), primary_key=True - ), + Column("interface_id", Integer, ForeignKey(_schema + ".interface.id"), primary_key=True), + Column("theme_id", Integer, ForeignKey(_schema + ".theme.id"), primary_key=True), schema=_schema ) @@ -774,11 +713,11 @@ class Interface(Base): backref="interfaces", cascade="save-update,merge,refresh-expire" ) - def __init__(self, name="", description=""): + def __init__(self, name: str="", description: str="") -> None: self.name = name self.description = description - def __unicode__(self): # pragma: no cover + def __unicode__(self) -> str: # pragma: no cover return self.name or "" @@ -794,10 +733,7 @@ class Metadata(Base): value = Column(Unicode) description = Column(Unicode) - item_id = Column( - "item_id", Integer, - ForeignKey(_schema + ".treeitem.id"), nullable=False - ) + item_id = Column("item_id", Integer, ForeignKey(_schema + ".treeitem.id"), nullable=False) item = relationship( "TreeItem", backref=backref( @@ -806,11 +742,11 @@ class Metadata(Base): ), ) - def __init__(self, name="", value=""): + def __init__(self, name: str="", value: str="") -> None: self.name = name self.value = value - def __unicode__(self): # pragma: no cover + def __unicode__(self) -> str: # pragma: no cover return "{0!s}: {1!s}".format(self.name or "", self.value or "") @@ -831,10 +767,7 @@ class Dimension(Base): value = Column(Unicode) description = Column(Unicode) - layer_id = Column( - "layer_id", Integer, - ForeignKey(_schema + ".layer.id"), nullable=False - ) + layer_id = Column("layer_id", Integer, ForeignKey(_schema + ".layer.id"), nullable=False) layer = relationship( "DimensionLayer", backref=backref( @@ -843,11 +776,11 @@ class Dimension(Base): ), ) - def __init__(self, name="", value="", layer=None): + def __init__(self, name: str="", value: str="", layer: str=None) -> None: self.name = name self.value = value if layer is not None: self.layer = layer - def __unicode__(self): # pragma: no cover + def __unicode__(self) -> str: # pragma: no cover return self.name or "" diff --git a/commons/c2cgeoportal_commons/models/sqlalchemy.py b/commons/c2cgeoportal_commons/models/sqlalchemy.py index 8e23d8ae96..273443eb56 100644 --- a/commons/c2cgeoportal_commons/models/sqlalchemy.py +++ b/commons/c2cgeoportal_commons/models/sqlalchemy.py @@ -26,7 +26,9 @@ # The views and conclusions contained in the software and documentation are those # of the authors and should not be interpreted as representing official policies, # either expressed or implied, of the FreeBSD Project. +from typing import Optional +from sqlalchemy.engine import Dialect from sqlalchemy.types import TypeDecorator, VARCHAR import json @@ -34,18 +36,16 @@ # get from http://docs.sqlalchemy.org/en/latest/orm/extensions/ # mutable.html#establishing-mutability-on-scalar-column-values class JSONEncodedDict(TypeDecorator): - "Represents an immutable structure as a json-encoded string." + """ + Represents an immutable structure as a json-encoded string. + """ impl = VARCHAR @staticmethod - def process_bind_param(value, dialect): - if value is not None: - value = json.dumps(value) - return value + def process_bind_param(value: Optional[dict], _: Dialect)-> Optional[str]: + return json.dumps(value) if value is not None else None @staticmethod - def process_result_value(value, dialect): - if value is not None: - value = json.loads(value) - return value + def process_result_value(value: Optional[str], _: Dialect) -> Optional[dict]: + return json.loads(value) if value is not None else None diff --git a/commons/c2cgeoportal_commons/models/static.py b/commons/c2cgeoportal_commons/models/static.py index 6d79f1be48..da8e3ee216 100644 --- a/commons/c2cgeoportal_commons/models/static.py +++ b/commons/c2cgeoportal_commons/models/static.py @@ -30,6 +30,7 @@ import logging from hashlib import sha1 +from typing import Optional from sqlalchemy import Column from sqlalchemy.types import Integer, Boolean, Unicode, String, DateTime @@ -49,7 +50,7 @@ from pyramid.i18n import TranslationStringFactory _ = TranslationStringFactory("c2cgeoportal") except ImportError: - def _(s): + def _(s: str) -> str: return s LOG = logging.getLogger(__name__) @@ -73,62 +74,46 @@ class User(Base): 'title': _('User'), 'plural': _('Users') } - item_type = Column( - "type", String(10), nullable=False, - info={ - 'colanderalchemy': { - 'widget': HiddenWidget() - } + item_type = Column("type", String(10), nullable=False, info={ + 'colanderalchemy': { + 'widget': HiddenWidget() } - ) + }) __mapper_args__ = { "polymorphic_on": item_type, "polymorphic_identity": "user", } - id = Column( - Integer, primary_key=True, - info={ - 'colanderalchemy': { - 'widget': HiddenWidget() - } + id = Column(Integer, primary_key=True, info={ + 'colanderalchemy': { + 'widget': HiddenWidget() } - ) - username = Column( - Unicode, unique=True, nullable=False, - ) - _password = Column( - "password", Unicode, nullable=False, - info={'colanderalchemy': {'widget': HiddenWidget()}} - ) - temp_password = Column( - "temp_password", Unicode, nullable=True, - ) + }) + username = Column(Unicode, unique=True, nullable=False) + _password = Column("password", Unicode, nullable=False, info={ + 'colanderalchemy': {'widget': HiddenWidget()} + }) + temp_password = Column("temp_password", Unicode, nullable=True) email = Column(Unicode, nullable=False, info={ 'colanderalchemy': { 'title': _('email') } }) - is_password_changed = Column( - Boolean, default=False, - info={ - 'colanderalchemy': {'widget': CheckboxWidget(readonly=True)} - } - ) - role_name = Column( - String, info={ - 'colanderalchemy': { - 'widget': deform_ext.RelationSelect2Widget( - Role, 'name', 'name', order_by='name', default_value=('', _('- Select -')) - ) - } + is_password_changed = Column(Boolean, default=False, info={ + 'colanderalchemy': {'widget': CheckboxWidget(readonly=True)} + }) + role_name = Column(String, info={ + 'colanderalchemy': { + 'widget': deform_ext.RelationSelect2Widget( + Role, 'name', 'name', order_by='name', default_value=('', _('- Select -')) + ) } - ) - _cached_role_name = None - _cached_role = None + }) + _cached_role_name = None # type: str + _cached_role = None # type: Optional[Role] @property - def role(self): + def role(self) -> Optional[Role]: if self._cached_role_name == self.role_name: return self._cached_role @@ -149,37 +134,36 @@ def role(self): return self._cached_role def __init__( - self, username="", password="", email="", is_password_changed=False, - functionalities=None, role=None - ): - if functionalities is None: - functionalities = [] + self, username: str="", password: str="", email: str="", is_password_changed: bool=False, + role: Role=None + ) -> None: self.username = username self.password = password self.email = email self.is_password_changed = is_password_changed - self.functionalities = functionalities if role is not None: self.role_name = role.name - def _get_password(self): + @property + def password(self) -> str: """returns password""" return self._password # pragma: no cover - def _set_password(self, password): + @password.setter + def password(self, password: str) -> None: """encrypts password on the fly.""" self._password = self.__encrypt_password(password) - def set_temp_password(self, password): + def set_temp_password(self, password: str) -> None: """encrypts password on the fly.""" self.temp_password = self.__encrypt_password(password) @staticmethod - def __encrypt_password(password): + def __encrypt_password(password: str) -> str: """Hash the given password with SHA1.""" return sha1(password.encode("utf8")).hexdigest() - def validate_password(self, passwd): + def validate_password(self, passwd: str) -> bool: """Check the password against existing credentials. this method _MUST_ return a boolean. @@ -200,9 +184,7 @@ def validate_password(self, passwd): return True return False - password = property(_get_password, _set_password) - - def __unicode__(self): + def __unicode__(self) -> str: return self.username or "" # pragma: no cover diff --git a/commons/c2cgeoportal_commons/scripts/initializedb.py b/commons/c2cgeoportal_commons/scripts/initializedb.py index e2ea8e1bb5..409ec23729 100644 --- a/commons/c2cgeoportal_commons/scripts/initializedb.py +++ b/commons/c2cgeoportal_commons/scripts/initializedb.py @@ -1,35 +1,37 @@ import os import sys +from typing import List + import transaction from logging.config import fileConfig from pyramid.paster import get_appsettings from pyramid.scripts.common import parse_vars +from sqlalchemy.engine import Connection +from sqlalchemy.orm import Session -from ..models import Base -from ..models import ( - get_engine, - get_session_factory, - get_tm_session, - generate_mappers, +from c2cgeoportal_commons.models import ( + Base, get_engine, get_session_factory, get_tm_session, generate_mappers, ) -def usage(argv): +def usage(argv: List[str]) -> None: cmd = os.path.basename(argv[0]) - print('usage: %s [var=value]\n' - '(example: "%s development.ini")' % (cmd, cmd)) + print( + 'usage: %s [var=value]\n' + '(example: "%s development.ini")' % (cmd, cmd) + ) sys.exit(1) -def main(argv=sys.argv): +def main(argv: List[str]=sys.argv) -> None: if len(argv) < 2: usage(argv) config_uri = argv[1] options = parse_vars(argv[2:]) - fileConfig(config_uri, defaults=os.environ) + fileConfig(config_uri, defaults=dict(os.environ)) options.update(os.environ) settings = get_appsettings(config_uri, options=options) generate_mappers(settings) @@ -37,26 +39,29 @@ def main(argv=sys.argv): engine = get_engine(settings) with engine.begin() as connection: - init_db(connection, - force='--force' in options, - test='--test' in options) + init_db( + connection, + force='--force' in options, + test='--test' in options + ) - ''' + """ # generate the Alembic version table and stamp it with the latest revision alembic_cfg = Config('alembic.ini') alembic_cfg.set_section_option( 'alembic', 'sqlalchemy.url', engine.url.__str__()) command.stamp(alembic_cfg, 'head') - ''' + """ -def init_db(connection, force=False, test=False): - from ..models import main # noqa: F401 - from ..models import static # noqa: F401 - from ..models import schema +def init_db(connection: Connection, force: bool=False, test: bool=False) -> None: + import c2cgeoportal_commons.models.main # noqa: F401 + import c2cgeoportal_commons.models.static # noqa: F401 + from c2cgeoportal_commons.models import schema schema_static = "{}_static".format(schema) + assert schema is not None if force: if schema_exists(connection, schema): connection.execute("DROP SCHEMA {} CASCADE;".format(schema)) @@ -79,7 +84,7 @@ def init_db(connection, force=False, test=False): setup_test_data(dbsession) -def schema_exists(connection, schema_name): +def schema_exists(connection: Connection, schema_name: str) -> bool: sql = ''' SELECT count(*) AS count FROM information_schema.schemata @@ -90,19 +95,19 @@ def schema_exists(connection, schema_name): return row[0] == 1 -def setup_test_data(dbsession): - from c2cgeoportal_commons.models.main import ( - Role - ) - from c2cgeoportal_commons.models.static import ( - User - ) +def setup_test_data(dbsession: Session) -> None: + from c2cgeoportal_commons.models.main import Role + from c2cgeoportal_commons.models.static import User role_admin = dbsession.merge(Role(name='role_admin')) role_user = dbsession.merge(Role(name='role_user')) - dbsession.merge(User(username='admin', - email='admin@camptocamp.com', - role=role_admin)) - dbsession.merge(User(username='user', - email='user@camptocamp.com', - role=role_user)) + dbsession.merge(User( + username='admin', + email='admin@camptocamp.com', + role=role_admin + )) + dbsession.merge(User( + username='user', + email='user@camptocamp.com', + role=role_user + )) diff --git a/commons/c2cgeoportal_commons/tests/__init__.py b/commons/c2cgeoportal_commons/tests/__init__.py deleted file mode 100644 index eb38f0e8cb..0000000000 --- a/commons/c2cgeoportal_commons/tests/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -import pytest -import transaction -from c2cgeoportal_commons.scripts.initializedb import init_db -from c2cgeoportal_commons.models import ( - get_engine, - get_session_factory, - get_tm_session, - generate_mappers, -) -from sqlalchemy.exc import DBAPIError - - -@pytest.fixture(scope='session') -@pytest.mark.usefixtures("settings") -def dbsession(settings): - generate_mappers(settings) - engine = get_engine(settings) - init_db(engine, force=True) - session_factory = get_session_factory(engine) - session = get_tm_session(session_factory, transaction.manager) - return session - - -@pytest.fixture(scope='function') -@pytest.mark.usefixtures("dbsession") -def transact(dbsession): - t = dbsession.begin_nested() - yield - t.rollback() - - -def raise_db_error(Table): - raise DBAPIError('this is a test !', None, None) - - -@pytest.fixture(scope='function') -@pytest.mark.usefixtures("dbsession") -def raise_db_error_on_query(dbsession): - query = dbsession.query - dbsession.query = raise_db_error - yield - dbsession.query = query diff --git a/docker-compose-run b/docker-compose-run index 4bd70c0c90..ef34f0a4e0 100755 --- a/docker-compose-run +++ b/docker-compose-run @@ -15,7 +15,10 @@ os.environ["RUN"] = urllib.parse.quote(json.dumps(sys.argv[1:])) if os.path.exists(".SUCCESS"): os.remove(".SUCCESS") -subprocess.check_call(["docker-compose", "up", "--abort-on-container-exit"]) +try: + subprocess.check_call(["docker-compose", "up", "--abort-on-container-exit"]) +except subprocess.CalledProcessError: + exit(2) if os.path.exists(".SUCCESS"): os.remove(".SUCCESS") diff --git a/docker/build/Dockerfile b/docker/build/Dockerfile index c0e85144b5..8555d1b4dc 100644 --- a/docker/build/Dockerfile +++ b/docker/build/Dockerfile @@ -20,6 +20,8 @@ COPY requirements.txt fixversions.txt /tmp/ RUN \ cd /tmp && \ pip install --disable-pip-version-check --no-cache-dir --requirement requirements.txt && \ + # for mypy + touch /usr/local/lib/python3.6/site-packages/zope/__init__.py && \ rm --recursive --force /tmp/* /var/tmp/* /root/.cache/* COPY run /usr/bin/ diff --git a/docker/build/requirements.txt b/docker/build/requirements.txt index 6453805a0f..38dbe8e5df 100644 --- a/docker/build/requirements.txt +++ b/docker/build/requirements.txt @@ -14,6 +14,7 @@ defusedxml==0.5.0 # geoportal dogpile.cache==0.6.4 # geoportal flake8==3.5.0 # lint flake8-copyright==0.2.0 # lint +flake8-mypy==17.3.3 # lint Fiona==1.7.10.post1 # geoportal-raster GeoAlchemy2==0.4.0 # geoportal geojson==2.3.0 # geoportal @@ -26,6 +27,7 @@ Jinja2==2.9.6 # c2c.template JSTools==1.0 # CGXP build lingua==4.13 # i18n mccabe==0.6.1 # lint +mypy==0.521 # lint OWSLib==0.15.0 # geoportal papyrus==2.2 # geoportal PasteScript==2.0.2 # geoportal-pcreate diff --git a/geoportal/c2cgeoportal_geoportal/lib/dbreflection.py b/geoportal/c2cgeoportal_geoportal/lib/dbreflection.py index 7d8d376c34..a40bf23af6 100644 --- a/geoportal/c2cgeoportal_geoportal/lib/dbreflection.py +++ b/geoportal/c2cgeoportal_geoportal/lib/dbreflection.py @@ -29,11 +29,13 @@ import warnings +from typing import Dict, Tuple # noqa, pylint: disable=unused-import from sqlalchemy import Table, sql, MetaData from sqlalchemy.orm import relationship from sqlalchemy.orm.util import class_mapper from sqlalchemy.exc import SAWarning +from sqlalchemy.ext.declarative.api import DeclarativeMeta # noqa, pylint: disable=unused-import from geoalchemy2 import Geometry @@ -41,14 +43,11 @@ from papyrus.xsd import tag -_class_cache = {} +_class_cache = {} # type: Dict[Tuple[str, str, str], DeclarativeMeta] SQL_GEOMETRY_COLUMNS = """ - SELECT - srid, - type - FROM - geometry_columns + SELECT srid, type + FROM geometry_columns WHERE f_table_schema = :table_schema AND f_table_name = :table_name AND diff --git a/geoportal/c2cgeoportal_geoportal/lib/lingua_extractor.py b/geoportal/c2cgeoportal_geoportal/lib/lingua_extractor.py index f255a57861..52510f136d 100644 --- a/geoportal/c2cgeoportal_geoportal/lib/lingua_extractor.py +++ b/geoportal/c2cgeoportal_geoportal/lib/lingua_extractor.py @@ -34,6 +34,7 @@ import yaml import re import traceback +from typing import Dict # noqa, pylint: disable=unused-import from json import loads from urllib.parse import urlsplit from defusedxml.minidom import parseString @@ -269,8 +270,8 @@ class GeoMapfishThemeExtractor(Extractor): # pragma: no cover # Run on the development.ini file extensions = [".ini"] - featuretype_cache = {} - wmscap_cache = {} + featuretype_cache = {} # type: Dict[str, Dict] + wmscap_cache = {} # type: Dict[str, WebMapService] def __call__(self, filename, options): messages = [] diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose-run b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose-run index e2e687aa8f..d1f9213f2f 100755 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose-run +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/create/docker-compose-run @@ -15,9 +15,12 @@ os.environ["RUN"] = urllib.parse.quote(json.dumps(sys.argv[1:])) if os.path.exists(".SUCCESS"): os.remove(".SUCCESS") -subprocess.check_call([ - "docker-compose", "--file", "docker-compose-build.yaml", "up", "--abort-on-container-exit" -]) +try: + subprocess.check_call([ + "docker-compose", "--file", "docker-compose-build.yaml", "up", "--abort-on-container-exit" + ]) +except subprocess.CalledProcessError: + exit(2) if os.path.exists(".SUCCESS"): os.remove(".SUCCESS") diff --git a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker.mk b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker.mk index 312c1a1c8d..4dee12e9b3 100644 --- a/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker.mk +++ b/geoportal/c2cgeoportal_geoportal/scaffolds/nondockercreate/nondocker.mk @@ -137,7 +137,7 @@ ifeq ($(OPERATING_SYSTEM), WINDOWS) .build/venv/Scripts/python -m pip install wheels/Shapely-1.5.13-cp27-none-win32.whl else # FIXME c2cgeoform - .build/venv/bin/python -m pip install `./get-pip-dependencies pyramid-closure c2cgeoportal-commons c2cgeoportal-geoportal GDAL c2cgeoform` + .build/venv/bin/python -m pip install `./get-pip-dependencies pyramid-closure c2cgeoportal-commons c2cgeoportal-geoportal GDAL c2cgeoform flake8-mypy mypy` endif ./docker-run cp -r /opt/c2cgeoportal_commons c2cgeoportal_commons ./docker-run cp -r /opt/c2cgeoportal_geoportal c2cgeoportal_geoportal diff --git a/geoportal/c2cgeoportal_geoportal/views/mapserverproxy.py b/geoportal/c2cgeoportal_geoportal/views/mapserverproxy.py index 76a1080eb1..f6a4250726 100644 --- a/geoportal/c2cgeoportal_geoportal/views/mapserverproxy.py +++ b/geoportal/c2cgeoportal_geoportal/views/mapserverproxy.py @@ -29,8 +29,11 @@ import logging +from typing import Any, Dict # noqa, pylint: disable=unused-import +from pyramid.response import Response from pyramid.view import view_config +from pyramid.request import Request from c2cgeoportal_geoportal.lib.caching import get_region, NO_CACHE, PUBLIC_CACHE, PRIVATE_CACHE from c2cgeoportal_geoportal.lib.functionality import get_mapserver_substitution_params @@ -44,13 +47,14 @@ class MapservProxy(OGCProxy): - def __init__(self, request): + params = {} # type: Dict[str, str] + + def __init__(self, request: Request) -> None: OGCProxy.__init__(self, request) self.user = self.request.user @view_config(route_name="mapserverproxy") - def proxy(self): - + def proxy(self) -> Response: if self.user is not None: # We have a user logged in. We need to set group_id and # possible layer_name in the params. We set layer_name @@ -146,7 +150,9 @@ def proxy(self): ) return response - def _proxy_callback(self, role_id, cache_control, url, params, **kwargs): + def _proxy_callback( + self, role_id: int, cache_control: int, url: str, params: dict, **kwargs: Any + ) -> Response: callback = params.get("callback") if callback is not None: del params["callback"] diff --git a/geoportal/c2cgeoportal_geoportal/views/ogcproxy.py b/geoportal/c2cgeoportal_geoportal/views/ogcproxy.py index 44a826db49..0c0aea1fc7 100644 --- a/geoportal/c2cgeoportal_geoportal/views/ogcproxy.py +++ b/geoportal/c2cgeoportal_geoportal/views/ogcproxy.py @@ -28,6 +28,7 @@ # either expressed or implied, of the FreeBSD Project. import logging +from typing import Dict # noqa, pylint: disable=unused-import from sqlalchemy.orm.exc import NoResultFound from c2cgeoportal_geoportal.lib import get_url2 @@ -42,6 +43,8 @@ class OGCProxy(Proxy): + params = {} # type: Dict[str, str] + def __init__(self, request): Proxy.__init__(self, request) diff --git a/geoportal/c2cgeoportal_geoportal/views/raster.py b/geoportal/c2cgeoportal_geoportal/views/raster.py index c56345511f..e61afd2fa5 100644 --- a/geoportal/c2cgeoportal_geoportal/views/raster.py +++ b/geoportal/c2cgeoportal_geoportal/views/raster.py @@ -44,9 +44,6 @@ class Raster: - # cache of GeoRaster instances in function of the layer name - _rasters = {} - def __init__(self, request): self.request = request self.rasters = self.request.registry.settings["raster"] diff --git a/geoportal/tests/functional/test_mapserverproxy.py b/geoportal/tests/functional/test_mapserverproxy.py index 53fcaf644c..6e68568242 100644 --- a/geoportal/tests/functional/test_mapserverproxy.py +++ b/geoportal/tests/functional/test_mapserverproxy.py @@ -58,6 +58,7 @@ # import hashlib +import typing from unittest import TestCase from sqlalchemy import Column, types @@ -77,7 +78,7 @@ TWO_POINTS = ["0a4fac2209d06c6fa36048c125b1679a", "0469e20ee04f22ab7ccdfebaa125f203"] NO_POINT = ["ef33223235b26c782736c88933b35331", "aaa27d9450664d34fd8f53b6e76af1e1"] -Base = sqlalchemy.ext.declarative.declarative_base() +Base: typing.Any = sqlalchemy.ext.declarative.declarative_base() class PointTest(Base): diff --git a/geoportal/tests/functional/test_themes_time.py b/geoportal/tests/functional/test_themes_time.py index 6a3aeb2975..77b7c0c633 100644 --- a/geoportal/tests/functional/test_themes_time.py +++ b/geoportal/tests/functional/test_themes_time.py @@ -29,6 +29,7 @@ import re +import typing import transaction from unittest import TestCase @@ -48,7 +49,7 @@ import logging log = logging.getLogger(__name__) -Base = sqlalchemy.ext.declarative.declarative_base() +Base: typing.Any = sqlalchemy.ext.declarative.declarative_base() class PointTest(Base):