# HG changeset patch # User Fabien Amarger <fabien.amarger@logilab.fr> # Date 1703001540 -3600 # Tue Dec 19 16:59:00 2023 +0100 # Node ID ec90aa5c440f84108e37ead3f96740ca8c2e903f # Parent 0000000000000000000000000000000000000000 feat: First commit first features diff --git a/cubicweb-rodolf/.gitlab-ci.yml b/cubicweb-rodolf/.gitlab-ci.yml new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/.gitlab-ci.yml @@ -0,0 +1,24 @@ +--- +default: + image: python:3.7 + +include: + - project: "open-source/gitlab-ci-templates" + ref: "branch/default" + file: + - "templates/no-duplicated-ci-pipelines.yml" # use workflow to avoid duplicated pipelines + - "templates/lint/flake8.yml" # will do the equivalent of 'tox -e flake8' + - "templates/lint/black.yml" # will do the equivalent of 'tox -e black' + - "templates/lint/check-manifest.yml" # will do the equivalent of 'tox -e check-manifest' + - "templates/lint/yamllint.yml" # will do the equivalent of 'tox -e yamllint' + - "templates/tests/py3.yml" # will do the equivalent of 'tox -e py3' + - "templates/create-release-on-heptapod.yml" # this will create a release on heptapod + - "templates/upload-to-pypi.yml" # on a new mercurial tag (expected to be done with release-new), will push a release on pypi + # - "templates/lint/mypy.yml" # will do the equivalent of 'tox -e mypy' + # - "templates/upload-python-package-to-heptapod.yml" # upload the package to heptapod registry on new tag + +stages: + - lint + - tests + - release + - publish diff --git a/cubicweb-rodolf/.hgignore b/cubicweb-rodolf/.hgignore new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/.hgignore @@ -0,0 +1,28 @@ +syntax: glob +*~ +#* +*.bak +*.old +*.pyo +*.pyc +*.cache +*.swp +*.swo +*.mo +*.lock +*.sqlite +__pycache__ +*.egg-info +.tox/ +.pybuild +.cache +.coverage +.pytest-cache +build/ +dist/ +node_modules/ +.mypy_cache/ +test/data/database/ +test/data/ldapdb/ +test/data/uicache/ +docs/_build diff --git a/cubicweb-rodolf/.yamllint b/cubicweb-rodolf/.yamllint new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/.yamllint @@ -0,0 +1,9 @@ +ignore: | + .tox/ + node_modules/ + +extends: default + +rules: + document-start: disable + line-length: disable diff --git a/cubicweb-rodolf/Dockerfile b/cubicweb-rodolf/Dockerfile new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/Dockerfile @@ -0,0 +1,9 @@ +FROM logilab/cubicweb-base:latest + +COPY --chown=cubicweb . /src +# If you pinned dependencies in a file (requirements.txt), add it here with +# -r /src/requirements.txt +RUN pip install -e /src + +USER cubicweb +RUN docker-cubicweb-helper create-instance diff --git a/cubicweb-rodolf/MANIFEST.in b/cubicweb-rodolf/MANIFEST.in new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/MANIFEST.in @@ -0,0 +1,11 @@ +include *.py +include */*.py +recursive-include cubicweb_rodolf *.py +recursive-include cubicweb_rodolf/data *.gif *.png *.ico *.css *.js +recursive-include cubicweb_rodolf/i18n *.po +recursive-include cubicweb_rodolf/wdoc * +recursive-include test/data bootstrap_cubes *.py +include *.ini +exclude .gitlab-ci.yml +exclude .yamllint +exclude Dockerfile diff --git a/cubicweb-rodolf/README.rst b/cubicweb-rodolf/README.rst new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/README.rst @@ -0,0 +1,57 @@ +rodolf +============================================================= + +RDF data production monitoring (RODOLF) + +Installation +------------ + +Open the project in a terminal and run:: + + pip install -e . + +This will install the cube in your active virtual environment +as ``cubicweb-rodolf``. + +The following sections indicate additional steps when you +install this cube as a dependency or as an instance. + +As a dependency +~~~~~~~~~~~~~~~ + +If you plan to use this cube as a dependency for your own cube, +add it to your ``__pkginfo__.py`` as follows:: + + __depends__ = { + # ... Your previous dependencies + "cubicweb-rodolf": None, + } + +If the target cube is already used as an instance, you need to migrate it +with the help of its python shell (replace ``YOUR_INSTANCE_NAME`` by your instance name):: + + cubicweb-ctl shell YOUR_INSTANCE_NAME + +In the python prompt, enter the following command:: + + add_cube("rodolf") + +Press ``Ctrl-D`` then restart your instance. +The cube should now be available in your instance. + +As an instance +~~~~~~~~~~~~~~ + +If you plan to use this cube directly as an instance, create and start +your instance with the following commands (replace ``rodolf-instance`` +by the name of your choice):: + + cubicweb-ctl create rodolf rodolf-instance + cubicweb-ctl start -D rodolf-instance + + +Learn More +---------- + +Visit the `official documentation <https://cubicweb.readthedocs.io/en/4.5.2>`_ +to learn more about CubicWeb. diff --git a/cubicweb-rodolf/cubicweb_rodolf/__init__.py b/cubicweb-rodolf/cubicweb_rodolf/__init__.py new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/cubicweb_rodolf/__init__.py @@ -0,0 +1,8 @@ +"""cubicweb-rodolf application package + +RDF data production monitoring (RODOLF) +""" + + +def includeme(config): + pass diff --git a/cubicweb-rodolf/cubicweb_rodolf/__pkginfo__.py b/cubicweb-rodolf/cubicweb_rodolf/__pkginfo__.py new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/cubicweb_rodolf/__pkginfo__.py @@ -0,0 +1,24 @@ +"""cubicweb-rodolf application packaging information""" + + +modname = "cubicweb_rodolf" +distname = "cubicweb-rodolf" + +numversion = (0, 1, 0) +version = ".".join(str(num) for num in numversion) + +license = "LGPL" +author = "LOGILAB S.A. (Paris, FRANCE)" +author_email = "contact@logilab.fr" +description = "RDF data production monitoring (RODOLF)" +web = "https://forge.extranet.logilab.fr/cubicweb/cubes/rodolf" + +__depends__ = {"cubicweb": ">= 4.5.2, < 5.0.0"} +__recommends__ = {} + +classifiers = [ + "Environment :: Web Environment", + "Framework :: CubicWeb", + "Programming Language :: Python :: 3", + "Programming Language :: JavaScript", +] diff --git a/cubicweb-rodolf/cubicweb_rodolf/entities.py b/cubicweb-rodolf/cubicweb_rodolf/entities.py new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/cubicweb_rodolf/entities.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# copyright 2023 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact https://www.logilab.fr -- mailto:contact@logilab.fr +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +"""cubicweb-rodolf entity's classes""" + +from datetime import datetime, timedelta +import pytz +from cubicweb import NoResultError + +from cubicweb.entities import AnyEntity + + +TIMEDELTA_REFRESH = { + "daily": timedelta(days=1), + "weekly": timedelta(weeks=1), + "monthly": timedelta(days=30), +} + + +class DataService(AnyEntity): + __regid__ = "DataService" + + @property + def refresh_timedelta(self): + return TIMEDELTA_REFRESH[self.refresh_period] + + +class ImportProcedure(AnyEntity): + __regid__ = "ImportProcedure" + + @property + def import_recipe_to_launch(self): + for recipe in self.import_recipes: + if not recipe.must_be_launched(self): + continue + yield recipe + + def create_needed_import_process(self): + for recipe in self.import_recipe_to_launch: + import_process = self._cw.create_entity( + "ImportProcess", + import_recipe=(recipe,), + import_procedure=(self,), + ) + print( + f"ImportProcess for {self.sparql_endpoint} (recipe : {recipe.name})" + f" created ({import_process.eid})" + ) + yield import_process + + +class ImportRecipe(AnyEntity): + __regid__ = "ImportRecipe" + + def must_be_launched(self, import_procedure): + try: + import_process = self._cw.execute( + "Any I ORDERBY D DESC LIMIT 1 WHERE " + "I is ImportProcess, " + "I import_recipe %(recipe_eid)s, " + "I import_procedure %(import_procedure)s, " + "I in_state S, S name 'successful', " + "I creation_date D", + { + "recipe_eid": self.eid, + "import_procedure": import_procedure.eid, + }, + ).one() + dataservice = import_process.import_recipe[0].dataservice[0] + if ( + datetime.now(tz=pytz.utc) - import_process.creation_date + > dataservice.refresh_timedelta + ): + return True + except NoResultError: + return True + return False diff --git a/cubicweb-rodolf/cubicweb_rodolf/hooks.py b/cubicweb-rodolf/cubicweb_rodolf/hooks.py new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/cubicweb_rodolf/hooks.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# copyright 2023 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact https://www.logilab.fr -- mailto:contact@logilab.fr +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +"""cubicweb-rodolf specific hooks and operations""" diff --git a/cubicweb-rodolf/cubicweb_rodolf/i18n/en.po b/cubicweb-rodolf/cubicweb_rodolf/i18n/en.po new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/cubicweb_rodolf/i18n/en.po @@ -0,0 +1,9 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: pygettext.py 1.5\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" + diff --git a/cubicweb-rodolf/cubicweb_rodolf/i18n/es.po b/cubicweb-rodolf/cubicweb_rodolf/i18n/es.po new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/cubicweb_rodolf/i18n/es.po @@ -0,0 +1,9 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: pygettext.py 1.5\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" + diff --git a/cubicweb-rodolf/cubicweb_rodolf/i18n/fr.po b/cubicweb-rodolf/cubicweb_rodolf/i18n/fr.po new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/cubicweb_rodolf/i18n/fr.po @@ -0,0 +1,9 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: pygettext.py 1.5\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" + diff --git a/cubicweb-rodolf/cubicweb_rodolf/migration/postcreate.py b/cubicweb-rodolf/cubicweb_rodolf/migration/postcreate.py new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/cubicweb_rodolf/migration/postcreate.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# copyright 2023 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact https://www.logilab.fr -- mailto:contact@logilab.fr +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +"""cubicweb-rodolf postcreate script, executed at instance creation time or when +the cube is added to an existing instance. + +You could setup site properties or a workflow here for example. +""" + +# Example of site property change +# set_property('ui.site-title', "<sitename>") + +from cubicweb_rodolf.workflows import create_import_process_workflow + + +create_import_process_workflow(add_workflow) diff --git a/cubicweb-rodolf/cubicweb_rodolf/schema.py b/cubicweb-rodolf/cubicweb_rodolf/schema.py new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/cubicweb_rodolf/schema.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# copyright 2023 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact https://www.logilab.fr -- mailto:contact@logilab.fr +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +"""cubicweb-rodolf schema""" + +from yams.buildobjs import Boolean, EntityType, String, SubjectRelation +from cubicweb.schema import WorkflowableEntityType +from cubicweb_file.schema import File + + +class SHACLFile(EntityType): + file = File() + name = String() + + +class ImportProcedure(EntityType): + name = String() + sparql_endpoint = String(required=True) + ontology_file = File() + shacl_files = SubjectRelation("SHACLFile", cardinality="**") + import_recipes = SubjectRelation("ImportRecipe", cardinality="**") + activated = Boolean(required=True, default=True) + + +class DataService(EntityType): + name = String(required=True) + data_url = String(required=True) + refresh_period = String( + required=True, + vocabulary=["daily", "weekly", "monthly"], + default="daily", + ) + description = String() + + +class ImportRecipe(EntityType): + name = String(required=True) + dataservice = SubjectRelation("DataService", cardinality="1*") + process_type = String( + required=True, + vocabulary=["default", "default-dryrun"], + default="default", + ) + + +class ImportProcess(WorkflowableEntityType): + import_recipe = SubjectRelation("ImportRecipe", cardinality="1*") + import_procedure = SubjectRelation("ImportProcedure", cardinality="1*") + log = File() + has_output_dataset = File() + import_report = File() diff --git a/cubicweb-rodolf/cubicweb_rodolf/views.py b/cubicweb-rodolf/cubicweb_rodolf/views.py new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/cubicweb_rodolf/views.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# copyright 2023 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact https://www.logilab.fr -- mailto:contact@logilab.fr +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +"""cubicweb-rodolf views/forms/actions/components for web ui""" diff --git a/cubicweb-rodolf/cubicweb_rodolf/workflows.py b/cubicweb-rodolf/cubicweb_rodolf/workflows.py new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/cubicweb_rodolf/workflows.py @@ -0,0 +1,12 @@ + +def create_import_process_workflow(add_workflow): + workflow = add_workflow("import process workflow", "ImportProcess") + waiting = workflow.add_state("waiting", initial=True) + ongoing = workflow.add_state("ongoing") + error = workflow.add_state("error") + successful = workflow.add_state("successful") + + workflow.add_transition("starts", (waiting,), ongoing) + workflow.add_transition("fails", (ongoing,), error) + workflow.add_transition("success", (ongoing,), successful) + return workflow diff --git a/cubicweb-rodolf/setup.py b/cubicweb-rodolf/setup.py new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/setup.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +# pylint: disable=W0142,W0403,W0404,W0613,W0622,W0622,W0704,R0904,C0103,E0611 +# +# copyright 2023 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact https://www.logilab.fr -- mailto:contact@logilab.fr +# +# This file is part of a cubicweb-rodolf. +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +"""cubicweb_rodolf setup module using data from +cubicweb_rodolf/__pkginfo__.py file +""" + +from os.path import join, dirname + +from setuptools import find_packages, setup + +here = dirname(__file__) + +# load metadata from the __pkginfo__.py file so there is no risk of conflict +# see https://packaging.python.org/en/latest/single_source_version.html +pkginfo = join(here, "cubicweb_rodolf", "__pkginfo__.py") +__pkginfo__ = {} +with open(pkginfo) as f: + exec(f.read(), __pkginfo__) + +# get required metadatas +distname = __pkginfo__["distname"] +version = __pkginfo__["version"] +license = __pkginfo__["license"] +description = __pkginfo__["description"] +web = __pkginfo__["web"] +author = __pkginfo__["author"] +author_email = __pkginfo__["author_email"] +classifiers = __pkginfo__["classifiers"] + +with open(join(here, "README.rst")) as f: + long_description = f.read() + +# get optional metadatas +data_files = __pkginfo__.get("data_files", None) +dependency_links = __pkginfo__.get("dependency_links", ()) + +requires = {} +for entry in ("__depends__",): # "__recommends__"): + requires.update(__pkginfo__.get(entry, {})) +install_requires = [ + "{0} {1}".format(d, v and v or "").strip() for d, v in requires.items() +] + + +setup( + name=distname, + version=version, + license=license, + description=description, + long_description=long_description, + long_description_content_type="text/x-rst", + author=author, + author_email=author_email, + url=web, + classifiers=classifiers, + packages=find_packages(exclude=["test"]), + install_requires=install_requires, + include_package_data=True, + entry_points={ + "cubicweb.cubes": [ + "rodolf=cubicweb_rodolf", + ], + }, + zip_safe=False, +) diff --git a/cubicweb-rodolf/test/data/bootstrap_cubes b/cubicweb-rodolf/test/data/bootstrap_cubes new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/test/data/bootstrap_cubes @@ -0,0 +1,1 @@ +rodolf diff --git a/cubicweb-rodolf/test/test_rodolf.py b/cubicweb-rodolf/test/test_rodolf.py new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/test/test_rodolf.py @@ -0,0 +1,29 @@ +# copyright 2023 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact https://www.logilab.fr -- mailto:contact@logilab.fr +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 2.1 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + + +from cubicweb.devtools import testlib + + +class DefaultTC(testlib.CubicWebTC): + def test_something(self): + self.skipTest("this cube has no test") + + +if __name__ == "__main__": + from unittest import main + + main() diff --git a/cubicweb-rodolf/tox.ini b/cubicweb-rodolf/tox.ini new file mode 100644 --- /dev/null +++ b/cubicweb-rodolf/tox.ini @@ -0,0 +1,77 @@ +[tox] +envlist = py3,flake8,check-manifest,black + +[testenv] +deps = + pytest +commands = + {envpython} -m pytest {posargs:test} + +[testenv:flake8] +basepython = python3 +skip_install = true +deps = + flake8 >= 3.6 +commands = flake8 + +[testenv:check-manifest] +skip_install = true +deps = + check-manifest +commands = + {envpython} -m check_manifest {toxinidir} + +[testenv:mypy] +deps = + mypy >= 0.761 +commands = mypy --ignore-missing-imports cubicweb_rodolf + +[testenv:black] +basepython = python3 +skip_install = true +deps = + black >= 22.12 +commands = black --check . + +[testenv:black-run] +basepython = python3 +skip_install = true +deps = + black >= 22.12 +commands = black . + +[testenv:pypi-publish] +basepython = python3 +skip_install = true +allowlist_externals = rm +deps = + twine +passenv = + TWINE_USERNAME + TWINE_PASSWORD +commands = + rm -rf build dist .egg .egg-info + python3 setup.py sdist bdist_wheel + twine check dist/* + twine upload --skip-existing dist/* + +[testenv:yamllint] +skip_install = true +deps = yamllint +commands = + yamllint . + +[testenv:release-new] +basepython = python3 +skip_install = true +passenv = + EDITOR +deps = + release-new +commands = release-new {posargs:-r auto} + +[flake8] +basepython = python3 +ignore = W503, E203, E731, E231 +max-line-length = 100 +exclude = cubicweb_rodolf/migration/*,test/data/*,.tox/* diff --git a/requirement.txt b/requirement.txt new file mode 100644 --- /dev/null +++ b/requirement.txt @@ -0,0 +1,2 @@ +cubicweb[postgresql] +cubicweb-file