scivetra 0.1.0a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scivetra-0.1.0a1/PKG-INFO +65 -0
- scivetra-0.1.0a1/README.md +53 -0
- scivetra-0.1.0a1/pyproject.toml +22 -0
- scivetra-0.1.0a1/setup.cfg +4 -0
- scivetra-0.1.0a1/src/scivetra/__init__.py +2 -0
- scivetra-0.1.0a1/src/scivetra/__main__.py +2 -0
- scivetra-0.1.0a1/src/scivetra/cli.py +92 -0
- scivetra-0.1.0a1/src/scivetra.egg-info/PKG-INFO +65 -0
- scivetra-0.1.0a1/src/scivetra.egg-info/SOURCES.txt +11 -0
- scivetra-0.1.0a1/src/scivetra.egg-info/dependency_links.txt +1 -0
- scivetra-0.1.0a1/src/scivetra.egg-info/entry_points.txt +2 -0
- scivetra-0.1.0a1/src/scivetra.egg-info/top_level.txt +1 -0
- scivetra-0.1.0a1/tests/test_inventory.py +62 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scivetra
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Deterministic, read-only inventory of local PDF files
|
|
5
|
+
Author: tvquynh
|
|
6
|
+
Project-URL: Source, https://github.com/tvquynh/scivetra
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# SciVetra bootstrap
|
|
14
|
+
|
|
15
|
+
SciVetra 0.1.0a1 provides a small, standard-library-only local PDF inventory utility.
|
|
16
|
+
It lists relative filenames, byte sizes, and SHA-256 digests as deterministic JSON.
|
|
17
|
+
It does not parse or execute PDFs, contact a network service, or upload files.
|
|
18
|
+
|
|
19
|
+
```console
|
|
20
|
+
scivetra --version
|
|
21
|
+
scivetra pdf-inventory ./my-local-fixtures
|
|
22
|
+
scivetra pdf-inventory ./my-local-fixtures --output inventory.json
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Choose only a local directory you are authorized to inspect. No location is scanned
|
|
26
|
+
by default. Keep the selected tree stable while scanning. Symlinks and Windows
|
|
27
|
+
reparse points are skipped; linked directory ancestors and Windows network drives
|
|
28
|
+
are rejected. On POSIX, use a local filesystem, not a mounted network filesystem.
|
|
29
|
+
Limits: 100,000 filesystem entries, 1 GB per PDF and 10 GB total PDF bytes.
|
|
30
|
+
Unchanged trees produce the same filename-sorted JSON. Errors exit with status 2;
|
|
31
|
+
no incomplete JSON is emitted. Output files must be new `.json` files; existing
|
|
32
|
+
files are never overwritten. This utility is not a sandbox against adversarial
|
|
33
|
+
concurrent filesystem changes.
|
|
34
|
+
|
|
35
|
+
This independent bootstrap does not implement SLR, RAG, AI, Crossref integration,
|
|
36
|
+
PDF downloading, or the future SciVetra research framework. Framework architecture
|
|
37
|
+
and subsequent implementation remain with the primary implementation engineer.
|
|
38
|
+
No software license has been selected or asserted for this bootstrap.
|
|
39
|
+
|
|
40
|
+
## Development
|
|
41
|
+
|
|
42
|
+
```console
|
|
43
|
+
python -m pip install -e .
|
|
44
|
+
python -m unittest discover -s tests -v
|
|
45
|
+
python -m pip install build twine
|
|
46
|
+
python -m build
|
|
47
|
+
python -m twine check dist/*
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## One-time initial publication
|
|
51
|
+
|
|
52
|
+
The manual `publish.yml` workflow accepts exactly `scivetra==0.1.0a1` as its
|
|
53
|
+
confirmation and runs only on `main`. Tests, wheel/sdist build, metadata validation,
|
|
54
|
+
and a fresh wheel CLI smoke test must pass before a separate OIDC publishing job.
|
|
55
|
+
Configure the GitHub `pypi` environment with required reviewer `tvquynh` and only
|
|
56
|
+
`main` allowed to deploy. PyPI's pending GitHub publisher must match owner
|
|
57
|
+
`tvquynh`, repository `scivetra`, workflow `publish.yml`, environment `pypi`.
|
|
58
|
+
Use no API token. Do not rerun an uncertain upload: check production PyPI and
|
|
59
|
+
Actions first. A pending publisher does not reserve the name.
|
|
60
|
+
|
|
61
|
+
First run the workflow with Publish unchecked (checks only). After inspecting the
|
|
62
|
+
successful checks, run once with Publish checked and the exact confirmation, then
|
|
63
|
+
approve the protected pypi deployment. A separate clean environment verifies the
|
|
64
|
+
production installation after upload. If upload succeeds but verification fails,
|
|
65
|
+
do not publish again; investigate the existing production release.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# SciVetra bootstrap
|
|
2
|
+
|
|
3
|
+
SciVetra 0.1.0a1 provides a small, standard-library-only local PDF inventory utility.
|
|
4
|
+
It lists relative filenames, byte sizes, and SHA-256 digests as deterministic JSON.
|
|
5
|
+
It does not parse or execute PDFs, contact a network service, or upload files.
|
|
6
|
+
|
|
7
|
+
```console
|
|
8
|
+
scivetra --version
|
|
9
|
+
scivetra pdf-inventory ./my-local-fixtures
|
|
10
|
+
scivetra pdf-inventory ./my-local-fixtures --output inventory.json
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Choose only a local directory you are authorized to inspect. No location is scanned
|
|
14
|
+
by default. Keep the selected tree stable while scanning. Symlinks and Windows
|
|
15
|
+
reparse points are skipped; linked directory ancestors and Windows network drives
|
|
16
|
+
are rejected. On POSIX, use a local filesystem, not a mounted network filesystem.
|
|
17
|
+
Limits: 100,000 filesystem entries, 1 GB per PDF and 10 GB total PDF bytes.
|
|
18
|
+
Unchanged trees produce the same filename-sorted JSON. Errors exit with status 2;
|
|
19
|
+
no incomplete JSON is emitted. Output files must be new `.json` files; existing
|
|
20
|
+
files are never overwritten. This utility is not a sandbox against adversarial
|
|
21
|
+
concurrent filesystem changes.
|
|
22
|
+
|
|
23
|
+
This independent bootstrap does not implement SLR, RAG, AI, Crossref integration,
|
|
24
|
+
PDF downloading, or the future SciVetra research framework. Framework architecture
|
|
25
|
+
and subsequent implementation remain with the primary implementation engineer.
|
|
26
|
+
No software license has been selected or asserted for this bootstrap.
|
|
27
|
+
|
|
28
|
+
## Development
|
|
29
|
+
|
|
30
|
+
```console
|
|
31
|
+
python -m pip install -e .
|
|
32
|
+
python -m unittest discover -s tests -v
|
|
33
|
+
python -m pip install build twine
|
|
34
|
+
python -m build
|
|
35
|
+
python -m twine check dist/*
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## One-time initial publication
|
|
39
|
+
|
|
40
|
+
The manual `publish.yml` workflow accepts exactly `scivetra==0.1.0a1` as its
|
|
41
|
+
confirmation and runs only on `main`. Tests, wheel/sdist build, metadata validation,
|
|
42
|
+
and a fresh wheel CLI smoke test must pass before a separate OIDC publishing job.
|
|
43
|
+
Configure the GitHub `pypi` environment with required reviewer `tvquynh` and only
|
|
44
|
+
`main` allowed to deploy. PyPI's pending GitHub publisher must match owner
|
|
45
|
+
`tvquynh`, repository `scivetra`, workflow `publish.yml`, environment `pypi`.
|
|
46
|
+
Use no API token. Do not rerun an uncertain upload: check production PyPI and
|
|
47
|
+
Actions first. A pending publisher does not reserve the name.
|
|
48
|
+
|
|
49
|
+
First run the workflow with Publish unchecked (checks only). After inspecting the
|
|
50
|
+
successful checks, run once with Publish checked and the exact confirmation, then
|
|
51
|
+
approve the protected pypi deployment. A separate clean environment verifies the
|
|
52
|
+
production installation after upload. If upload succeeds but verification fails,
|
|
53
|
+
do not publish again; investigate the existing production release.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scivetra"
|
|
7
|
+
version = "0.1.0a1"
|
|
8
|
+
description = "Deterministic, read-only inventory of local PDF files"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [{name = "tvquynh"}]
|
|
12
|
+
dependencies = []
|
|
13
|
+
classifiers = ["Development Status :: 3 - Alpha", "Programming Language :: Python :: 3", "Operating System :: OS Independent"]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Source = "https://github.com/tvquynh/scivetra"
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
scivetra = "scivetra.cli:main"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["src"]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Inventory only explicitly selected, stable local directories."""
|
|
2
|
+
import argparse
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import stat
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
|
|
12
|
+
MAX_ENTRIES = 100_000
|
|
13
|
+
MAX_FILE_BYTES = 1_000_000_000
|
|
14
|
+
MAX_TOTAL_BYTES = 10_000_000_000
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def linked(info):
|
|
18
|
+
return stat.S_ISLNK(info.st_mode) or bool(getattr(info, "st_file_attributes", 0) & 0x400)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def inventory(directory):
|
|
22
|
+
root = Path(os.path.abspath(directory))
|
|
23
|
+
if str(root).startswith(("\\\\", "//")):
|
|
24
|
+
raise ValueError("Network paths are unsupported")
|
|
25
|
+
if os.name == "nt":
|
|
26
|
+
import ctypes
|
|
27
|
+
if ctypes.windll.kernel32.GetDriveTypeW(str(root.anchor)) != 3:
|
|
28
|
+
raise ValueError("Choose a local fixed drive")
|
|
29
|
+
for part in (*reversed(root.parents), root):
|
|
30
|
+
if linked(part.lstat()):
|
|
31
|
+
raise ValueError("Directory ancestors must not be symlinks or reparse points")
|
|
32
|
+
if not root.is_dir():
|
|
33
|
+
raise ValueError("Expected a directory")
|
|
34
|
+
rows, pending = [], [root]
|
|
35
|
+
entries = total = 0
|
|
36
|
+
while pending:
|
|
37
|
+
current = pending.pop()
|
|
38
|
+
with os.scandir(current) as children:
|
|
39
|
+
for child in children:
|
|
40
|
+
entries += 1
|
|
41
|
+
if entries > MAX_ENTRIES:
|
|
42
|
+
raise ValueError("Entry limit exceeded")
|
|
43
|
+
info = os.stat(child.path, follow_symlinks=False)
|
|
44
|
+
if linked(info):
|
|
45
|
+
continue
|
|
46
|
+
if stat.S_ISDIR(info.st_mode):
|
|
47
|
+
pending.append(Path(child.path))
|
|
48
|
+
elif stat.S_ISREG(info.st_mode) and child.name.lower().endswith(".pdf"):
|
|
49
|
+
if info.st_size > MAX_FILE_BYTES or total + info.st_size > MAX_TOTAL_BYTES:
|
|
50
|
+
raise ValueError("PDF byte limit exceeded")
|
|
51
|
+
flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
52
|
+
fd = os.open(child.path, flags)
|
|
53
|
+
with os.fdopen(fd, "rb") as stream:
|
|
54
|
+
before = os.fstat(stream.fileno())
|
|
55
|
+
if linked(before) or not stat.S_ISREG(before.st_mode) or (before.st_dev, before.st_ino) != (info.st_dev, info.st_ino):
|
|
56
|
+
raise ValueError("File changed during inventory")
|
|
57
|
+
digest = hashlib.sha256()
|
|
58
|
+
count = 0
|
|
59
|
+
while chunk := stream.read(1024 * 1024):
|
|
60
|
+
count += len(chunk)
|
|
61
|
+
if count > MAX_FILE_BYTES or total + count > MAX_TOTAL_BYTES:
|
|
62
|
+
raise ValueError("PDF byte limit exceeded")
|
|
63
|
+
digest.update(chunk)
|
|
64
|
+
after = os.fstat(stream.fileno())
|
|
65
|
+
if count != info.st_size or (after.st_size, after.st_mtime_ns) != (info.st_size, info.st_mtime_ns):
|
|
66
|
+
raise ValueError("File changed during inventory")
|
|
67
|
+
total += count
|
|
68
|
+
rows.append({"filename": Path(child.path).relative_to(root).as_posix(), "size_bytes": count, "sha256": digest.hexdigest()})
|
|
69
|
+
return sorted(rows, key=lambda row: row["filename"])
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def main(argv=None):
|
|
73
|
+
parser = argparse.ArgumentParser(description="Read-only inventory of explicitly selected local PDFs; no parsing or network access.")
|
|
74
|
+
parser.add_argument("--version", action="version", version=f"scivetra {__version__}")
|
|
75
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
76
|
+
pdf = commands.add_parser("pdf-inventory")
|
|
77
|
+
pdf.add_argument("directory")
|
|
78
|
+
pdf.add_argument("--output", type=Path)
|
|
79
|
+
args = parser.parse_args(argv)
|
|
80
|
+
try:
|
|
81
|
+
result = json.dumps(inventory(args.directory), ensure_ascii=True, indent=2) + "\n"
|
|
82
|
+
if args.output:
|
|
83
|
+
if args.output.suffix.lower() != ".json":
|
|
84
|
+
raise ValueError("Output must use a .json extension")
|
|
85
|
+
# Exclusive creation prevents overwriting any user file or symlink target.
|
|
86
|
+
with args.output.open("x", encoding="utf-8", newline="\n") as output:
|
|
87
|
+
output.write(result)
|
|
88
|
+
else:
|
|
89
|
+
sys.stdout.write(result)
|
|
90
|
+
except (OSError, ValueError) as error:
|
|
91
|
+
parser.exit(2, f"scivetra: {error}\n")
|
|
92
|
+
return 0
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scivetra
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: Deterministic, read-only inventory of local PDF files
|
|
5
|
+
Author: tvquynh
|
|
6
|
+
Project-URL: Source, https://github.com/tvquynh/scivetra
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# SciVetra bootstrap
|
|
14
|
+
|
|
15
|
+
SciVetra 0.1.0a1 provides a small, standard-library-only local PDF inventory utility.
|
|
16
|
+
It lists relative filenames, byte sizes, and SHA-256 digests as deterministic JSON.
|
|
17
|
+
It does not parse or execute PDFs, contact a network service, or upload files.
|
|
18
|
+
|
|
19
|
+
```console
|
|
20
|
+
scivetra --version
|
|
21
|
+
scivetra pdf-inventory ./my-local-fixtures
|
|
22
|
+
scivetra pdf-inventory ./my-local-fixtures --output inventory.json
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Choose only a local directory you are authorized to inspect. No location is scanned
|
|
26
|
+
by default. Keep the selected tree stable while scanning. Symlinks and Windows
|
|
27
|
+
reparse points are skipped; linked directory ancestors and Windows network drives
|
|
28
|
+
are rejected. On POSIX, use a local filesystem, not a mounted network filesystem.
|
|
29
|
+
Limits: 100,000 filesystem entries, 1 GB per PDF and 10 GB total PDF bytes.
|
|
30
|
+
Unchanged trees produce the same filename-sorted JSON. Errors exit with status 2;
|
|
31
|
+
no incomplete JSON is emitted. Output files must be new `.json` files; existing
|
|
32
|
+
files are never overwritten. This utility is not a sandbox against adversarial
|
|
33
|
+
concurrent filesystem changes.
|
|
34
|
+
|
|
35
|
+
This independent bootstrap does not implement SLR, RAG, AI, Crossref integration,
|
|
36
|
+
PDF downloading, or the future SciVetra research framework. Framework architecture
|
|
37
|
+
and subsequent implementation remain with the primary implementation engineer.
|
|
38
|
+
No software license has been selected or asserted for this bootstrap.
|
|
39
|
+
|
|
40
|
+
## Development
|
|
41
|
+
|
|
42
|
+
```console
|
|
43
|
+
python -m pip install -e .
|
|
44
|
+
python -m unittest discover -s tests -v
|
|
45
|
+
python -m pip install build twine
|
|
46
|
+
python -m build
|
|
47
|
+
python -m twine check dist/*
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## One-time initial publication
|
|
51
|
+
|
|
52
|
+
The manual `publish.yml` workflow accepts exactly `scivetra==0.1.0a1` as its
|
|
53
|
+
confirmation and runs only on `main`. Tests, wheel/sdist build, metadata validation,
|
|
54
|
+
and a fresh wheel CLI smoke test must pass before a separate OIDC publishing job.
|
|
55
|
+
Configure the GitHub `pypi` environment with required reviewer `tvquynh` and only
|
|
56
|
+
`main` allowed to deploy. PyPI's pending GitHub publisher must match owner
|
|
57
|
+
`tvquynh`, repository `scivetra`, workflow `publish.yml`, environment `pypi`.
|
|
58
|
+
Use no API token. Do not rerun an uncertain upload: check production PyPI and
|
|
59
|
+
Actions first. A pending publisher does not reserve the name.
|
|
60
|
+
|
|
61
|
+
First run the workflow with Publish unchecked (checks only). After inspecting the
|
|
62
|
+
successful checks, run once with Publish checked and the exact confirmation, then
|
|
63
|
+
approve the protected pypi deployment. A separate clean environment verifies the
|
|
64
|
+
production installation after upload. If upload succeeds but verification fails,
|
|
65
|
+
do not publish again; investigate the existing production release.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/scivetra/__init__.py
|
|
4
|
+
src/scivetra/__main__.py
|
|
5
|
+
src/scivetra/cli.py
|
|
6
|
+
src/scivetra.egg-info/PKG-INFO
|
|
7
|
+
src/scivetra.egg-info/SOURCES.txt
|
|
8
|
+
src/scivetra.egg-info/dependency_links.txt
|
|
9
|
+
src/scivetra.egg-info/entry_points.txt
|
|
10
|
+
src/scivetra.egg-info/top_level.txt
|
|
11
|
+
tests/test_inventory.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
scivetra
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import hashlib
|
|
3
|
+
import io
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import tempfile
|
|
7
|
+
import unittest
|
|
8
|
+
from unittest.mock import patch
|
|
9
|
+
from scivetra.cli import inventory, main
|
|
10
|
+
|
|
11
|
+
class InventoryTests(unittest.TestCase):
|
|
12
|
+
def test_deterministic_nested_inventory_without_parsing(self):
|
|
13
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
14
|
+
root = Path(directory)
|
|
15
|
+
(root / "sub").mkdir()
|
|
16
|
+
(root / "sub" / "b.PDF").write_bytes(b"not a parsed PDF")
|
|
17
|
+
(root / "a.pdf").write_bytes(b"abc")
|
|
18
|
+
(root / "ignore.txt").write_bytes(b"ignored")
|
|
19
|
+
expected = [{"filename":"a.pdf", "size_bytes":3, "sha256":hashlib.sha256(b"abc").hexdigest()}, {"filename":"sub/b.PDF", "size_bytes":16, "sha256":hashlib.sha256(b"not a parsed PDF").hexdigest()}]
|
|
20
|
+
self.assertEqual(inventory(root), expected)
|
|
21
|
+
self.assertEqual(inventory(root), expected)
|
|
22
|
+
capture = io.StringIO()
|
|
23
|
+
with contextlib.redirect_stdout(capture):
|
|
24
|
+
self.assertEqual(main(["pdf-inventory", directory]), 0)
|
|
25
|
+
self.assertEqual(json.loads(capture.getvalue()), expected)
|
|
26
|
+
output = root / "inventory.json"
|
|
27
|
+
main(["pdf-inventory", directory, "--output", str(output)])
|
|
28
|
+
original = output.read_bytes()
|
|
29
|
+
with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit):
|
|
30
|
+
main(["pdf-inventory", directory, "--output", str(output)])
|
|
31
|
+
self.assertEqual(output.read_bytes(), original)
|
|
32
|
+
self.assertEqual((root / "a.pdf").read_bytes(), b"abc")
|
|
33
|
+
|
|
34
|
+
def test_limits_fail_closed(self):
|
|
35
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
36
|
+
(Path(directory)/"a.pdf").write_bytes(b"abc")
|
|
37
|
+
with patch("scivetra.cli.MAX_FILE_BYTES", 2), self.assertRaises(ValueError):
|
|
38
|
+
inventory(directory)
|
|
39
|
+
with patch("scivetra.cli.MAX_ENTRIES", 0), self.assertRaises(ValueError):
|
|
40
|
+
inventory(directory)
|
|
41
|
+
|
|
42
|
+
def test_symlinks_are_not_followed(self):
|
|
43
|
+
with tempfile.TemporaryDirectory() as directory, tempfile.TemporaryDirectory() as outside:
|
|
44
|
+
root = Path(directory)
|
|
45
|
+
(Path(outside)/"private.pdf").write_bytes(b"outside")
|
|
46
|
+
try:
|
|
47
|
+
(root/"linked").symlink_to(outside, target_is_directory=True)
|
|
48
|
+
(root/"linked.pdf").symlink_to(Path(outside)/"private.pdf")
|
|
49
|
+
except OSError:
|
|
50
|
+
self.skipTest("Symlink creation unavailable; run this test on Linux CI")
|
|
51
|
+
self.assertEqual(inventory(root), [])
|
|
52
|
+
with self.assertRaises(ValueError):
|
|
53
|
+
inventory(root/"linked")
|
|
54
|
+
|
|
55
|
+
def test_invalid_and_empty_directories(self):
|
|
56
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
57
|
+
self.assertEqual(inventory(directory), [])
|
|
58
|
+
with self.assertRaises(OSError):
|
|
59
|
+
inventory(Path(directory)/"missing")
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
unittest.main()
|