Skip to content

Commit 21a8982

Browse files
committed
Add partitioned storage for <=1000 files per folder
1 parent 6c4a470 commit 21a8982

5 files changed

Lines changed: 227 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Change log
22

3+
## Unreleased
4+
5+
Features:
6+
7+
- **Partitioned split structure:** The `--split` option now organizes concept files into subdirectories by ID range (1000 IDs per directory, e.g., `IDs0001xxx/`). This avoids GitHub UI limitations with large directories. The `--join` option supports both the new partitioned structure and the previous flat structure.
8+
39
## Release 1.0.0 (RC2) (2025-12-27)
410

511
The following changes were made to RC1 based on testing it with the voc4cat vocabulary and Skosmos 3.0.

docs/reference/cli.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,15 +117,20 @@ voc4cat transform [options] VOCAB
117117

118118
### Split/Join workflow
119119

120-
Large turtle files produce difficult-to-review git diffs. The split format stores each concept in a separate file, making changes easier to review:
120+
Large turtle files produce difficult-to-review git diffs. The split format stores each concept in a separate file, making changes easier to review.
121+
122+
Concepts are partitioned into subdirectories by ID range (1000 IDs per directory) to avoid GitHub UI limitations with large directories:
121123

122124
```
123125
vocabularies/myvocab/
124126
├── concept_scheme.ttl
125-
├── 0001001.ttl
126-
└── 0001002.ttl
127+
└── IDs0001xxx/
128+
├── 0001001.ttl
129+
└── 0001002.ttl
127130
```
128131

132+
The directory name padding matches the vocabulary's `id_length` setting (e.g., `IDs0001xxx` for 7-digit IDs, `IDs001xxx` for 6-digit IDs).
133+
129134
The voc4cat-template workflows use split format for storage and join files when needed for documentation or export.
130135

131136
### Examples

src/voc4cat/check.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def ci_post(args):
126126
if (
127127
prev_split_voc.exists()
128128
and prev_split_voc.is_dir()
129-
and any(prev_split_voc.glob("*.ttl"))
129+
and any(prev_split_voc.rglob("*.ttl"))
130130
):
131131
# Create a single vocab out of the directory
132132
logger.debug("-> previous version is a split vocabulary, joining...")

src/voc4cat/transform.py

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def add_prov_from_git(
144144
) -> None:
145145
"""Add dct:created and dct:modified to RDF files based on git history.
146146
147-
For each .ttl file in vocab_dir:
147+
For each .ttl file in vocab_dir (including subdirectories):
148148
- dct:created: Add only if missing (from git first commit date)
149149
- dct:modified: Update if different from git last commit date (logs info message)
150150
@@ -161,19 +161,20 @@ def add_prov_from_git(
161161
vocab_dir = Path(vocab_dir)
162162
git_lookup_dir = Path(source_dir) if source_dir else vocab_dir
163163

164-
# Get all .ttl files in the directory
165-
ttl_files = list(vocab_dir.glob("*.ttl"))
164+
# Get all .ttl files in the directory (including subdirectories)
165+
ttl_files = list(vocab_dir.rglob("*.ttl"))
166166
if not ttl_files:
167167
logger.warning("No .ttl files found in %s", vocab_dir)
168168
return
169169

170170
# Get git info from the source directory (or vocab_dir if no source specified)
171171
git_info = get_directory_git_info(git_lookup_dir, repo_dir)
172172

173-
# Check that all .ttl files are tracked in git (by filename)
173+
# Check that all .ttl files are tracked in git (by relative path)
174174
for ttl_file in ttl_files:
175-
# Build the path as it would appear in the source directory
176-
source_file = git_lookup_dir / ttl_file.name
175+
# Preserve subdirectory structure when looking up in source directory
176+
rel_to_vocab = ttl_file.relative_to(vocab_dir)
177+
source_file = git_lookup_dir / rel_to_vocab
177178
try:
178179
rel_path = source_file.relative_to(repo_dir)
179180
except ValueError:
@@ -186,8 +187,9 @@ def add_prov_from_git(
186187

187188
# Process each .ttl file
188189
for ttl_file in ttl_files:
189-
# Build the path as it would appear in the source directory for git lookup
190-
source_file = git_lookup_dir / ttl_file.name
190+
# Preserve subdirectory structure when looking up in source directory
191+
rel_to_vocab = ttl_file.relative_to(vocab_dir)
192+
source_file = git_lookup_dir / rel_to_vocab
191193
try:
192194
rel_path = source_file.relative_to(repo_dir)
193195
except ValueError:
@@ -306,14 +308,39 @@ def extract_numeric_id_from_iri(iri):
306308
return "".join(reversed(reverse_id))
307309

308310

311+
PARTITION_SIZE = 1000 # Number of IDs per subdirectory partition
312+
313+
314+
def get_partition_dir_name(numeric_id_str: str, id_length: int = 7) -> str:
315+
"""Compute subdirectory name for a given numeric ID.
316+
317+
Partitions IDs into subdirectories of PARTITION_SIZE (1000) IDs each.
318+
Directory name padding matches vocabulary's id_length:
319+
- 7-digit IDs: IDs0000xxx, IDs0001xxx, ...
320+
- 6-digit IDs: IDs000xxx, IDs001xxx, ...
321+
322+
Args:
323+
numeric_id_str: String representation of the numeric ID (e.g., "0000016")
324+
id_length: The configured ID length for the vocabulary (default 7)
325+
326+
Returns:
327+
Partition directory name (e.g., "IDs0000xxx" for 7-digit IDs)
328+
"""
329+
numeric_value = int(numeric_id_str) if numeric_id_str else 0
330+
partition_num = numeric_value // PARTITION_SIZE
331+
prefix_width = id_length - 3 # 'xxx' represents last 3 digits
332+
return f"IDs{partition_num:0{prefix_width}d}xxx"
333+
334+
309335
def write_split_turtle(
310336
vocab_graph: Graph, outdir: Path, vocab_name: str | None = None
311337
) -> None:
312338
"""
313339
Write each concept, collection and concept scheme to a separate turtle file.
314340
315341
The ids are used as filenames. Schema:Person and schema:Organization entities
316-
are included in the concept_scheme.ttl file.
342+
are included in the concept_scheme.ttl file. Concepts and collections are
343+
partitioned into subdirectories by ID range (1000 IDs per directory).
317344
318345
Args:
319346
vocab_graph: The vocabulary graph to split.
@@ -324,6 +351,13 @@ def write_split_turtle(
324351
outdir.mkdir(exist_ok=True)
325352
query = "SELECT ?iri WHERE {?iri a %s.}"
326353

354+
# Get id_length from config (default 7 if not configured)
355+
id_length = 7
356+
if vocab_name:
357+
vocab_config = config.IDRANGES.vocabs.get(vocab_name.lower())
358+
if vocab_config:
359+
id_length = vocab_config.id_length
360+
327361
for skos_class in ["skos:Concept", "skos:Collection", "skos:ConceptScheme"]:
328362
qresults = vocab_graph.query(query % skos_class, initNs={"skos": SKOS})
329363
# Iterate over search results and write each concept, collection and
@@ -343,7 +377,11 @@ def write_split_turtle(
343377
tmp_graph += vocab_graph.triples((entity_iri, None, None))
344378
outfile = outdir / "concept_scheme.ttl"
345379
else:
346-
outfile = outdir / f"{id_part}.ttl"
380+
# Partition concepts and collections into subdirectories by ID range
381+
partition_dir = get_partition_dir_name(id_part, id_length)
382+
partition_path = outdir / partition_dir
383+
partition_path.mkdir(exist_ok=True)
384+
outfile = partition_path / f"{id_part}.ttl"
347385
tmp_graph.serialize(destination=outfile, format="longturtle")
348386
logger.debug("-> wrote %i %ss-file(s).", len(qresults), skos_class)
349387

@@ -442,7 +480,7 @@ def transform(args):
442480
logger.warning("Unsupported filetype: %s", args.VOCAB)
443481

444482
if args.join:
445-
rdf_dirs = [d for d in Path(args.VOCAB).iterdir() if any(d.glob("*.ttl"))]
483+
rdf_dirs = [d for d in Path(args.VOCAB).iterdir() if any(d.rglob("*.ttl"))]
446484
else:
447485
rdf_dirs = []
448486

@@ -489,11 +527,11 @@ def transform(args):
489527
# Determine vocabulary directories to process:
490528
# - If VOCAB contains .ttl files directly, it's a single vocabulary directory
491529
# - Otherwise, look for subdirectories containing .ttl files (like vocabularies/)
492-
if any(args.VOCAB.glob("*.ttl")):
530+
if any(args.VOCAB.rglob("*.ttl")):
493531
vocab_dirs = [args.VOCAB]
494532
else:
495533
vocab_dirs = [
496-
d for d in args.VOCAB.iterdir() if d.is_dir() and any(d.glob("*.ttl"))
534+
d for d in args.VOCAB.iterdir() if d.is_dir() and any(d.rglob("*.ttl"))
497535
]
498536
if not vocab_dirs:
499537
msg = f'--prov-from-git requires a directory with .ttl files or subdirectories containing .ttl files, got: "{args.VOCAB}"'

0 commit comments

Comments
 (0)