python3 -m venv .venv
.venv/bin/pip install -e ../dgp -e . # local core library + this package
.venv/bin/pip install aiopg 'sqlalchemy<2' # aiopg isn't declared; see publishing.md for why <2The server needs a Postgres, because init_pg opens an aiopg engine at startup. A
throwaway cluster is enough:
initdb -D /tmp/pg -U postgres -A trust
pg_ctl -D /tmp/pg -o "-p 5499 -k '' -h 127.0.0.1" -l /tmp/pg.log start
# (-k '' avoids macOS's 103-byte limit on Unix socket paths when the data dir path is long)The server loads taxonomies/index.yaml relative to the working directory, and
writes the .cache/ preview cache there too. So run it from a directory that has
a taxonomies/ folder, such as dgp-app or datacity-importer, or a test fixture of
your own:
cd /path/with/taxonomies
BASE_PATH=/tmp/dgp DATABASE_URL=postgresql://postgres@127.0.0.1:5499/postgres \
/path/to/dgp-server/.venv/bin/python -m dgp_server.server # → http://127.0.0.1:8000/api
uid=$(curl -s -XPOST localhost:8000/api/config -H 'content-type: application/json' \
-d '{"source": {"path": "data.csv"}, "taxonomy": {"id": "my-taxonomy"}}' | jq -r .uid)
curl -N localhost:8000/api/events/$uid # the SSE stream; ends with "data: close"BASE_PATH/<uid>/config.yamlshows the analyzed config after the run.- Server logs are quiet unless you raise the level of the
dgp-serveranddgploggers. dgp-app does this inserver_dgp.py.
To run the real dgp-app subclass instead, run python server_dgp.py from dgp-app,
which serves on port 5001. It needs DATASETS_DATABASE_URL, plus an
ETLS_DATABASE_URL that has dgp-app's etl_pipeline and etl_taxonomies tables.
Most behaviour lives in the core library and can be driven in-process. This is
the same loop as DgpServer.events:
from dgp.core import Config, Context
from dgp.genera import SimpleDGP, LoaderDGP, PostLoaderDGP, TransformDGP, EnricherDGP
from dgp.taxonomies import TaxonomyRegistry
from dataflows import Flow
cfg = Config(None, data=my_dgp_config) # filename=None → nothing written to disk
ctx = Context(cfg, TaxonomyRegistry('taxonomies/index.yaml'))
for _ in range(5):
dgp = SimpleDGP(cfg, ctx, steps=[LoaderDGP, PostLoaderDGP, TransformDGP, EnricherDGP])
ok = dgp.analyze()
if not cfg.dirty:
break
print(ok, [str(e) for e in dgp.errors], cfg._unflatten()['model'])
rows, dp, _ = Flow(dgp.flow()).results()To test publishing, set cfg['publish.allowed'] = True, re-run the analysis,
then run Flow(dgp.flow(), publish_flow(cfg, create_engine(url), 'append')).process().
If a taxonomy module creates its DB engine at import time (datacity does), set its
env vars before you construct the TaxonomyRegistry.
- There are no tests.
pytest.iniandtox.inipoint attests/, which doesn't exist, somake test(tox,py37) has nothing to run. The coredgprepo has only one sample file,dgp/genera/test_sample.py. make lintrunspylama dgp_server(pyflakes, mccabe and pep8; max line length 120). Thedevelopextra installs pylama and tox.- To verify a change, use the recipes above: the curl/SSE run for protocol changes, the in-process loop for config and analysis changes, and SQLite or a scratch Postgres for publishing.
I found all of these in the code; the ones marked (verified) I reproduced.
Protocol and server:
- The flow runs synchronously inside the aiohttp handler and blocks that worker's event loop. Production uses 4 gunicorn workers to cope.
Configstarts out dirty, so there are always at least two analysis phases and at: cmessage. The dirty flag reflects only the most recentset(). (verified)- Requesting
/events/<uid>for an unknown uid returns onlyclose, with no error message. ConfigHeaderMappingsand the core map guesser mutate the shared taxonomy registry (header_mapping) on every request, and never remove entries.- The preview cache (
.cache/<hash>, relative to the working directory) is never invalidated when the data behind an unchanged URL changes. /configsandconfigurations.py's fallbackcreate_allreados.environ['DATABASE_URL'], which isn't set in dgp-app.config_storer.ConfigStorerDGPis unused.log.pyattaches a handler but sets no level, so INFO logs are dropped unless the host app sets one.
Configuration and mapping (core dgp):
- The
normalizeform of a header mapping is never applied by the guesser; you have to spell it out inmodel.mapping. (verified) constantshas to be a list of pairs. A dict is flattened away silently. (verified)structure.skip_colsis detected but never applied.source.headersconflicts with the loader's ownheadersargument.- A taxonomy's own
loading_module/preloading_moduleis ignored; only_common_'s is used. header_mappingaccepts only a single file.taxonomy.columnTypesis a per-pipeline snapshot, refreshed only in preview. A config that is published without a preview has no column types. (verified)
Publishing:
- On SQLAlchemy 2.x the
clear_by_sourceDELETE is rolled back. (verified) _sourceisn't in the primary key, and there's one table per taxonomy, so sources whose keys overlap collide. (verified formuni_budgets)source_field_namedoesn't rename the column that gets added.- The index creation is retried, and logged as an error, on every run.
- dgp-app's
FileLoaderDGPrewritessource.pathto a cache path that includes a timestamp, which makes_sourceunstable for re-uploaded files.
Packaging:
aiopgisn't declared as a dependency.- The Dockerfile is stale (
dgp_server==0.0.61, Python 3.7). - The classifiers and tox target Python 3.7, while production runs 3.12.
- Git tags stop at 0.0.61.