European Patent Office bulk data is useful and awkward at the same time. The BDDS product catalogue is large, each archive nests more archives, and the payloads that matter sit as XML exchange documents deep inside that tree. A naive download-and-unpack cycle fills disks and dies halfway through a multi-day run.
epo-processor is a streaming ETL for that problem. It pulls EPO archives from
the EPO BDDS API,
walks nested tar / gzip / zip containers without materialising whole trees,
parses exchange documents, and writes Parquet. A second lane streams the
HUPD all-years tarball to disk.
A third lane joins the two worlds: citation overlap between EPO records and a
local HUPD extraction, optionally as a linked Parquet dataset for downstream
models, search indexes, or review tools.
Three commands share one pipeline shape.
process turns EPO BDDS product archives into Parquet. Archives are listed
from the product catalogue, opened over HTTP (or replayed from a local
directory), unwrapped in stream, and parsed into patent rows. An optional
bbolt checkpoint records finished archives so a killed run continues instead of
starting over.
process-hupd materialises the HUPD Hugging Face tarball. Same opener and
walk machinery, but extraction and sink are no-ops: the goal is files on disk
for later analysis, not Parquet rows.
analyze builds the bridge. It indexes HUPD identifiers (Feather metadata
when you have it, otherwise a parallel JSON walk), streams the EPO Parquet or
CSV once, and reports citation overlap. Pass --dataset to emit a linked
Parquet where each retained EPO patent carries the HUPD paths and citation
categories that matched.
Default behaviour is fully streaming: raw archives and extracted entries are
not kept unless you opt in. Structured log/slog logs and end-of-run summary
tables make long jobs observable without drowning the terminal.
cd epo-processor
devenv shell
epo-build
epo-process # config/config.yaml
epo-process-hupd data/hupd # optional HUPD lane
epo-analyze data/hupd ./data.parquet \
--dataset data/epo_hupd_dataset.parquetThe project ships a Nix-backed devenv shell with Go tooling, git-cliff, and
container helpers. Enter the shell once, then use the epo-* scripts or Make.
# https://devenv.sh/getting-started/
curl -L https://get.devenv.sh | bash
cd epo-processor
devenv shell
make build # writes bin/epo-processorgit clone https://github.com/Qubut/epo-processor.git
cd epo-processor
go build -o bin/epo-processor ./cmd/epo_processor
./bin/epo-processor --helpgo install github.com/Qubut/epo-processor/cmd/epo_processor@latestEvery subcommand composes the same stages. Only the concrete implementations change.
| Stage | Contract | process |
process-hupd |
|---|---|---|---|
| Catalogue | ArchiveSource |
EPOProductSource |
StaticListSource |
| Open | ArchiveOpener |
HTTPOpener |
HTTPOpener |
| Extract | RecordExtractor |
XMLStreamExtractor |
NoopExtractor |
| Write | RecordSink |
ParquetSink |
NoopSink |
Stages talk over typed streams from
destel/rill, with backpressure and
context.Context cancellation. Sink writes are serial; archive and extractor
concurrency are the knobs that saturate cores and network.
Retention is a janitor concern, not buried inside openers. Flip
pipeline.keep_archive or pipeline.keep_extracted when you need the raw
bytes or XML on disk for debugging; leave both false for production streaming
runs.
Resumability is a decorator on the source. Set pipeline.checkpoint_db to a
bbolt path and completed archive IDs persist across process restarts. Leave it
empty for a no-op checkpointer with identical control flow and no resume state.
Global flags: --config, --log-level / -l, --log-dir.
| Command | Role |
|---|---|
process |
EPO BDDS into Parquet |
process-hupd <out-dir> |
HUPD .tar onto disk |
analyze <hupd-dir> [epo-file] |
Citation overlap and optional linked dataset |
version |
Build version |
config print |
Loaded config as JSON |
Downloads every archive in the configured BDDS product, unwraps nested containers, parses exchange documents, and batches rows into Parquet. Use a checkpoint when the run may span hours or days.
epo-process
# or:
epo-run process --config config/config.yaml -o ./data.parquetUseful flags: -o / --out, -c / --concurrency, --extract-concurrency,
--parser-concurrency, --batch-size, --batch-timeout, --row-group-size,
--spool-dir, --local-dir, --keep-archive, --archive-dir,
--keep-extracted, --extracted-dir, --checkpoint, --reset,
--base-url, --timeout, --max-retries, --product-id, --verify-sha1.
Streams the HUPD all-years tarball and writes unwrapped entries under
<out-dir>. Pass --keep-archive if you also want the raw .tar retained.
epo-process-hupd data/hupd
# or:
epo-run process-hupd data/hupd --keep-archive --archive-dir data/archivesUseful flags: -u / --url, --filename, --keep-archive, --archive-dir,
--spool-dir, --extract-concurrency, --max-retries.
Indexes HUPD, streams EPO once, and reports overlap. Positional arguments are
order-flexible: the directory is HUPD, the file is EPO. With a single argument,
that path is the HUPD directory and EPO defaults to the pipeline output path
from config (pipeline.output_parquet).
epo-analyze /data/hupd/extracted ./data.parquet \
--dataset data/epo_hupd_dataset.parquet \
-m /data/hupd/hupd_metadata.feather \
-o report.jsonUseful flags: -m / --meta, -o / --out, --dataset, --min-collisions,
-w / --workers.
Defaults live in config/config.yaml. Viper merges file, environment, and
flags. Prefer editing the YAML for stable runs; use flags for one-off overrides.
log:
log_level: info
log_dir: logs
server:
base_url: "https://publication-bdds.apps.epo.org/bdds/bdds-bff-service/prod/api/public"
product_id: 3
max_retries: 2
timeout: 30s
verify_sha1: false
hupd:
url: "https://huggingface.co/datasets/HUPD/hupd/resolve/main/data/all-years.tar"
filename: data/hupd_all-years.tar
analyze:
hupd_meta_url: "https://huggingface.co/datasets/HUPD/hupd/resolve/main/hupd_metadata_2022-02-22.feather"
pipeline:
archive_concurrency: 16
extractor_concurrency: 0 # 0 = auto from CPU and memory budget
parser_concurrency: 2
memory_budget_gb: 32
per_entry_estimate_mb: 256
batch_size: 100000
batch_timeout: 2s
output_parquet: "./data.parquet"
row_group_size: 500000
spool_dir: ""
use_local_dir: ""
keep_archive: false
archive_dir: "data/archives"
keep_extracted: false
extracted_dir: "data/xml"
checkpoint_db: "data/.epo-state.db"extractor_concurrency: 0 asks the planner to size in-flight XML readers from
runtime.NumCPU() clamped by memory_budget_gb and per_entry_estimate_mb.
Raise archive_concurrency if the network is idle while extractors wait; raise
the memory budget if auto sizing undershoots a large machine.
devenv shellScripts on $PATH (from devenv.nix):
| Script | What it runs |
|---|---|
epo-build |
make build |
epo-run [args] |
bin/epo-processor [args] (builds if needed) |
epo-process [args] |
process --config config/config.yaml |
epo-process-hupd [args] |
process-hupd --config config/config.yaml |
epo-analyze [args] |
analyze --config config/config.yaml |
epo-test |
go test -race ./... |
epo-container-build |
devenv container build prod |
epo-container-run [args] |
devenv container run prod -- [args] |
epo-changelog |
git-cliff -o CHANGELOG.md |
Makefile targets:
| Target | Description |
|---|---|
make build |
Build bin/epo-processor |
make test |
Tests with -race |
make test-cover |
Coverage HTML |
make lint |
golangci-lint |
make fmt |
gofumpt + goimports + golines |
make tidy |
go mod tidy + verify |
make build-all |
Cross-compile common GOOS/GOARCH |
make clean |
Remove bin/ and coverage.out |
make changelog |
Regenerate CHANGELOG.md |
make container-build |
Build prod OCI image |
make container-run |
Run prod image (ARGS='--help' make container-run) |
make dev |
Live reload via air |
make help |
List targets |
make all runs tidy, lint, test, then build. Default make prints help.
Package docs come from Go comments and examples. After a version tag is published they appear on pkg.go.dev:
https://pkg.go.dev/github.com/Qubut/epo-processor
go doc ./...
go doc github.com/Qubut/epo-processor/cmdThe prod image is a static binary plus CA certificates, built with devenv and nix2container. There is no Dockerfile.
devenv container build prod
devenv container run prod -- --help
docker pull ghcr.io/qubut/epo-processor:latestImages publish from container.yml on main and v* tags.
Commits follow Conventional Commits.
git-cliff regenerates CHANGELOG.md.
make changelogPushing a v* tag runs GoReleaser, refreshes CHANGELOG.md on main, and
publishes the container image.
- Fork and branch.
- Enter
devenv shell. - Ensure
make allpasses. - Open a PR that states what changed and why.
Commit style: <scope>: <imperative> <what>
(example: pipeline: tee kept entries).
MIT. See LICENSE.
| Resource | URL |
|---|---|
| EPO BDDS | https://www.epo.org/en/searching-for-patents/data/bulk-data-sets |
| HUPD | https://huggingface.co/datasets/HUPD/hupd |
| destel/rill | https://github.com/destel/rill |
| IBM/fp-go | https://github.com/IBM/fp-go |
| parquet-go | https://github.com/parquet-go/parquet-go |
| bbolt | https://github.com/etcd-io/bbolt |
| go-pretty | https://github.com/jedib0t/go-pretty |