ojs 0.7.2__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.
ojs-0.7.2/.env.example ADDED
@@ -0,0 +1,16 @@
1
+ # Copy to .env and fill in (or run `ojs init`). The CLI reads `.env` from the
2
+ # current directory, then falls back to ~/.config/ojs/.env (set OJS_CONFIG_PATH
3
+ # to point at a different file) for anything unset -- place this file at
4
+ # ~/.config/ojs/.env to use one config from any directory.
5
+ #
6
+ # .env is gitignored; keep it private -- it holds your API token. `ojs init`
7
+ # writes it 0600 on POSIX; on a shared host you can also `chmod 600 .env`. On
8
+ # Windows, file-mode restriction does not apply -- rely on directory/ACL access
9
+ # control.
10
+
11
+ # Required for the `ojs api` commands
12
+ OJS_BASE_URL=
13
+ OJS_API_KEY=
14
+
15
+ # Optional -- data directory override (default shown)
16
+ OJS_DATA_DIR=data/ojs-api
@@ -0,0 +1,10 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ - package-ecosystem: uv
8
+ directory: /
9
+ schedule:
10
+ interval: weekly
@@ -0,0 +1,38 @@
1
+ name: Publish
2
+
3
+ # Disabled by default. Publishing only runs once the repository variable
4
+ # PUBLISH_ENABLED is set to "true":
5
+ # gh variable set PUBLISH_ENABLED --body true # enable
6
+ # gh variable set PUBLISH_ENABLED --body false # disable (or `gh variable delete`)
7
+ # Until then, tag pushes trigger this workflow but both jobs skip.
8
+ on:
9
+ push:
10
+ tags: ["v*"]
11
+
12
+ jobs:
13
+ build:
14
+ if: vars.PUBLISH_ENABLED == 'true'
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v6
18
+ - uses: astral-sh/setup-uv@v8.2.0
19
+ - run: uv build
20
+ - uses: actions/upload-artifact@v7
21
+ with:
22
+ name: dist
23
+ path: dist/
24
+
25
+ publish:
26
+ needs: build
27
+ if: vars.PUBLISH_ENABLED == 'true'
28
+ runs-on: ubuntu-latest
29
+ environment: pypi
30
+ permissions:
31
+ id-token: write
32
+ contents: read
33
+ steps:
34
+ - uses: actions/download-artifact@v8
35
+ with:
36
+ name: dist
37
+ path: dist/
38
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,26 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [dev, main]
6
+ pull_request:
7
+ branches: [dev, main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ matrix:
17
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
18
+ steps:
19
+ - uses: actions/checkout@v6
20
+ - uses: astral-sh/setup-uv@v8.2.0
21
+ - run: uv python install ${{ matrix.python-version }}
22
+ - run: uv sync --all-groups --python ${{ matrix.python-version }}
23
+ - run: uv run ruff check .
24
+ - run: uv run ruff format --check .
25
+ - run: uv run pyrefly check
26
+ - run: uv run pytest --cov --cov-report=term-missing
ojs-0.7.2/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ .archive/
2
+ .claude/
3
+ .hypothesis/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .venv/
7
+ .vscode/
8
+ .worktrees/
9
+
10
+ __pycache__/
11
+ build/
12
+ dist/
13
+ *.egg-info/
14
+
15
+ .DS_Store
16
+
17
+ .coverage
18
+ .coverage.*
19
+
20
+ .env
21
+ .env.*
22
+ !.env.example
@@ -0,0 +1,23 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.15.15
4
+ hooks:
5
+ - id: ruff-format
6
+ - id: ruff
7
+ args: [--fix]
8
+ - repo: local
9
+ hooks:
10
+ - id: pyrefly-check
11
+ name: pyrefly check
12
+ entry: uv run pyrefly check
13
+ language: system
14
+ types_or: [python, pyi]
15
+ pass_filenames: false
16
+ require_serial: true
17
+ - repo: local
18
+ hooks:
19
+ - id: planners-validate
20
+ name: validate plan frontmatter
21
+ entry: planners validate
22
+ language: system
23
+ files: ^\.planners/plans/[^/]+/plan\.md$
@@ -0,0 +1 @@
1
+ 3.14
ojs-0.7.2/CHANGELOG.md ADDED
@@ -0,0 +1,44 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.7.1] - 2026-06-08
11
+
12
+ - `OJS_DOWNLOADS_DIR` now defaults to `data/ojs-website` (was `$OJS_DATA_DIR/website-downloads`), placing website CSV exports beside the API data rather than nested under it.
13
+
14
+ ## [0.7.0] - 2026-06-08
15
+
16
+ - Add a user-level config fallback (`~/.config/ojs/.env` by default, or the file named by `OJS_CONFIG_PATH`) for values not set in the local `.env`; drop the `ojs init --api-key` flag (the key comes only from `OJS_API_KEY` or the hidden prompt).
17
+
18
+ ## [0.6.0] - 2026-06-08
19
+
20
+ - Make the CSV export globs fixed package constants (drop the `OJS_ARTICLES_GLOB` / `OJS_REVIEWS_GLOB` env vars); default the data directory to `data/ojs-api` with the API JSON at its root; infer the `submission_files` schema from all rows.
21
+
22
+ ## [0.5.0] - 2026-06-05
23
+
24
+ - Add an `interval` column to the view-stats timelines so day/month points stay separable; write API JSON dumps atomically; always pull cumulative `publication_stats` in full; redact the API token from HTTP error messages; constrain file writes to the download tree; tolerate JSON `null` in normalization.
25
+
26
+ ## [0.4.0] - 2026-06-05
27
+
28
+ - Add the typed schema framework (`ojs/schema.py`: `Column`/`Table`) as the runtime source of truth for normalization, with `ojs api schema` exporting `table_schemas.csv`; move the website CSV pipelines under `ojs.website`.
29
+
30
+ ## [0.3.2] - 2026-06-05
31
+
32
+ - Log unservable files to `skipped.json`; drop zero-view days from the per-submission `views_timeline`; unwrap the `{items, itemsMax}` envelope for submission files.
33
+
34
+ ## [0.3.0] - 2026-06-01
35
+
36
+ - Add `ojs api download` for submission file artifacts (manifest-tracked, incremental) and a journal-wide `views_timeline_totals` table; normalize empty localized values to `null`.
37
+
38
+ ## [0.2.1] - 2026-05-28
39
+
40
+ - Bump runtime dependencies and dev tooling (polars, typer, ruff, pyrefly).
41
+
42
+ ## [0.2.0] - 2026-05-28
43
+
44
+ - Add incremental fetch (`ojs api fetch --incremental`/`--full`, with a sync-state watermark) and publication view stats (`publication_stats`, `views_timeline`).
ojs-0.7.2/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ronald E. Robertson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
ojs-0.7.2/PKG-INFO ADDED
@@ -0,0 +1,233 @@
1
+ Metadata-Version: 2.4
2
+ Name: ojs
3
+ Version: 0.7.2
4
+ Summary: Tools for working with the Open Journal Systems (OJS) API.
5
+ Project-URL: repository, https://github.com/gitronald/ojs
6
+ Author-email: gitronald <gitronald@users.noreply.github.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.11
10
+ Requires-Dist: httpx>=0.28.1
11
+ Requires-Dist: polars>=1.41.2
12
+ Requires-Dist: python-dotenv>=1.2.2
13
+ Requires-Dist: typer>=0.26.7
14
+ Description-Content-Type: text/markdown
15
+
16
+ # ojs
17
+
18
+ Tools for working with the Open Journal Systems (OJS) API.
19
+
20
+ Pulls submissions, publications, reviews, users, and publication view statistics
21
+ from an OJS journal's `/api/v1/*` REST API, downloads the attached file artifacts
22
+ (manuscripts, revisions, reviewer attachments, and production galleys), and
23
+ normalizes the JSON into typed relational tables backed by polars. Incremental
24
+ sync re-pulls only what changed since the last run, so routine top-ups stay
25
+ cheap. A typed schema layer (`Column`/`Table` classes) is the single source of
26
+ truth for normalization and doubles as exportable column documentation. Also
27
+ normalizes the OJS dashboard's Articles and Reviews CSV report exports. Ships a
28
+ Typer CLI for the common fetch, download, and normalize workflows. Built against
29
+ the [OJS 3.3 REST API](https://docs.pkp.sfu.ca/dev/api/ojs/3.3); other versions
30
+ are untested and may differ, as the REST API saw breaking changes between 3.2
31
+ and 3.3.
32
+
33
+ ## Project Structure
34
+
35
+ ```
36
+ ojs/
37
+ ├── cli.py # Typer CLI: init, articles, reviews, api (+ schema docs)
38
+ ├── schema.py # Typed schema framework: Column/Table, apply(), doc export
39
+ ├── utils.py # HTML stripping + localized-field extraction
40
+ ├── website/ # Manual website CSV-export pipelines
41
+ │ ├── articles/ # Wide CSV → submissions, authors, editors, decisions
42
+ │ └── reviews/ # Long CSV → reviews
43
+ └── api/ # REST pipeline
44
+ ├── client.py # OJS REST client (httpx, pagination, retry, early-stop)
45
+ ├── files.py # Submission file artifact downloads (disk layout, manifest)
46
+ ├── normalize.py # JSON → relational tables (schema-driven)
47
+ ├── schemas.py # API table schema classes
48
+ ├── sync.py # Incremental sync: high-water-mark state, raw-JSON upsert
49
+ └── swagger.json # OJS API reference (snapshot)
50
+ ```
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ uv tool install ojs
56
+ ```
57
+
58
+ As a project dependency:
59
+
60
+ ```bash
61
+ uv add ojs
62
+ ```
63
+
64
+ From GitHub instead of PyPI:
65
+
66
+ ```bash
67
+ uv tool install git+https://github.com/gitronald/ojs.git
68
+ # or, as a dependency: uv add git+https://github.com/gitronald/ojs.git
69
+ ```
70
+
71
+ From source (for development):
72
+
73
+ ```bash
74
+ git clone https://github.com/gitronald/ojs.git
75
+ cd ojs
76
+ uv sync
77
+ ```
78
+
79
+ ## Configuration
80
+
81
+ The CLI reads from a `.env` file in the current directory. Run `ojs init` to
82
+ scaffold one — it prompts for the journal URL and API token, and writes `.env`
83
+ with `0600` permissions:
84
+
85
+ ```bash
86
+ ojs init
87
+ ```
88
+
89
+ **Getting an API key.** In OJS, open your user profile
90
+ (`https://example.org/index.php/myjournal/user/profile`), select the **API Key**
91
+ tab, check **Enable external applications with the API key to access this
92
+ account**, and copy the key — use the **(re)generate** button if one isn't set
93
+ yet.
94
+
95
+ Values can also come from the environment. A user-level config file is loaded as
96
+ a fallback for anything not set in the current directory's `.env` (which takes
97
+ precedence): `~/.config/ojs/.env` by default, or the file named by
98
+ `OJS_CONFIG_PATH`.
99
+
100
+ | Variable | Default | Purpose |
101
+ | --- | --- | --- |
102
+ | `OJS_BASE_URL` | (required for `api`) | OJS journal URL (e.g. `https://example.org/index.php/myjournal`) |
103
+ | `OJS_API_KEY` | (required for `api`) | OJS API token |
104
+ | `OJS_DATA_DIR` | `data/ojs-api` | Root for inputs and outputs |
105
+ | `OJS_DOWNLOADS_DIR` | `data/ojs-website` | Where CSV exports land |
106
+ | `OJS_ARTICLES_DIR` | `$OJS_DATA_DIR/articles` | Articles output dir |
107
+ | `OJS_REVIEWS_DIR` | `$OJS_DATA_DIR/reviews` | Reviews output dir |
108
+ | `OJS_API_DIR` | `$OJS_DATA_DIR` | API JSON dump dir |
109
+ | `OJS_FILES_DIR` | `$OJS_API_DIR/files` | Where downloaded submission files land |
110
+
111
+ ## CLI Commands
112
+
113
+ `norm` reads the typed schema classes directly — no separate step is required.
114
+ `schema` exports a `table_schemas.csv` documenting each table's columns, dtypes,
115
+ source mapping, and whether each column appears in the normalized output
116
+ (`in_output`).
117
+
118
+ ### API
119
+
120
+ Fetch raw JSON from the REST API, download file artifacts, and normalize into
121
+ relational tables.
122
+
123
+ ```bash
124
+ ojs api fetch # fetch raw JSON from the OJS REST API
125
+ ojs api download # download submission file artifacts (PDFs, etc.)
126
+ ojs api norm # normalize API JSON into relational tables
127
+ ojs api schema # export table_schemas.csv docs
128
+ ```
129
+
130
+ ### Articles
131
+
132
+ Normalize the OJS dashboard's Articles Report CSV export.
133
+
134
+ ```bash
135
+ ojs articles norm # normalize the most recent articles export
136
+ ojs articles schema # export table_schemas.csv docs
137
+ ```
138
+
139
+ ### Reviews
140
+
141
+ Normalize the OJS dashboard's Review Report CSV export.
142
+
143
+ ```bash
144
+ ojs reviews norm # normalize the most recent reviews export
145
+ ojs reviews schema # export table_schemas.csv docs
146
+ ```
147
+
148
+ ### Article view stats
149
+
150
+ `ojs api fetch` also pulls publication view stats from the OJS `/stats/publications/*`
151
+ endpoints (skip with `--no-stats`). The API only exposes aggregated counts — the
152
+ finest granularity is **daily** (there are no per-event timestamps).
153
+
154
+ | Flag | Default | Purpose |
155
+ | --- | --- | --- |
156
+ | `--stats / --no-stats` | on | Toggle stats collection (e.g. when the API key lacks stats access) |
157
+ | `--stats-interval` | `day` | Timeline granularity: `day` or `month` |
158
+ | `--stats-since` | (none) | `dateStart` filter (`YYYY-MM-DD`) |
159
+ | `--stats-until` | (none) | `dateEnd` filter (`YYYY-MM-DD`) |
160
+
161
+ `ojs api norm` then writes three extra tables:
162
+
163
+ - `publication_stats` — one row per published submission with abstract, all-galley, PDF, HTML, and other view totals.
164
+ - `views_timeline` — long format (`submission_id`, `date`, `interval`, `views`, `kind`) with a per-submission abstract and galley series. `interval` records the granularity (`day` or `month`) a point was fetched at, so a file mixing both stays separable — filter on it rather than summing across intervals.
165
+ - `views_timeline_totals` — long format (`date`, `interval`, `views`, `kind`) with the journal-wide abstract and galley series, from the aggregate `/stats/publications/{abstract,galley}` endpoints (the data behind the OJS statistics-page graph). Use this for journal-wide totals rather than summing `views_timeline`.
166
+
167
+ If the API key lacks stats access, `fetch` prints a warning and skips the stats files, and `norm` simply omits the two tables.
168
+
169
+ ### Submission files
170
+
171
+ OJS attaches the actual file artifacts (manuscripts, revisions, reviewer
172
+ attachments, production galleys) to each submission. `ojs api download` fetches
173
+ their metadata and then downloads the binaries.
174
+
175
+ ```bash
176
+ ojs api fetch --files # also dump file metadata -> submission_files.json
177
+ ojs api download # download all files for all submissions
178
+ ojs api download -s 123 -s 456 # only these submissions (repeatable)
179
+ ojs api download --type galleys # only published galley files
180
+ ojs api download --type review # only review files / revisions / attachments
181
+ ojs api download --file-stage 4 --file-stage 15 # raw fileStage ids
182
+ ojs api download --no-revisions # current files only, skip prior revisions
183
+ ```
184
+
185
+ | Flag | Default | Purpose |
186
+ | --- | --- | --- |
187
+ | `--submission-id` / `-s` | all | Limit to these submission ids (repeatable) |
188
+ | `--type` | `all` | `all`, `galleys` (published), or `review` |
189
+ | `--file-stage` | (none) | Raw `fileStage` id(s); overrides `--type` |
190
+ | `--revisions / --no-revisions` | on | Also download prior revisions of each file |
191
+ | `--fetch / --no-fetch` | on | Refresh file metadata first (off: use stored JSON) |
192
+
193
+ Files are laid out under `OJS_FILES_DIR` as
194
+ `<submission_id>/<stage>/<fileId>_<name>`. A manifest (`manifest.json`) records
195
+ every artifact by its immutable physical `fileId`, so reruns skip files already
196
+ on disk — new uploads and revisions are downloaded incrementally.
197
+
198
+ **Rounds and revisions.** OJS tracks two distinct axes. A file's *stage*
199
+ (`fileStage`) says where in the workflow it lives; review files additionally
200
+ carry an `assocId` naming the **review round** they belong to. Separately, each
201
+ file's `revisions[]` holds prior uploads of that same logical file. `ojs api
202
+ norm` writes a `submission_files` table with one row per current file, including
203
+ `file_stage_label`, `review_round_id` (joins to `review_assignments.round_id`),
204
+ and `revision_count`. Downloads cover the current file plus every revision, each
205
+ keyed by its own `fileId`.
206
+
207
+ Downloading files requires an API token with permission to view them; the API
208
+ returns `403` for files the key cannot access.
209
+
210
+ ### Incremental fetch
211
+
212
+ By default `ojs api fetch` does a full cold pull. For routine top-ups, `--incremental` fetches only what changed since the last successful sync and merges it into the existing JSON dumps, so `ojs api norm` stays a stateless re-derivation from the complete files.
213
+
214
+ | Flag | Purpose |
215
+ | --- | --- |
216
+ | `--incremental` / `-i` | Fetch only records changed since the last sync, merging into the JSON dumps |
217
+ | `--since YYYY-MM-DD` | Override the stored watermark (implies `--incremental`) |
218
+ | `--full` | Force a complete pull and reset the sync state |
219
+
220
+ How it works:
221
+
222
+ - A high-water mark lives in `data/ojs-api/sync_state.json` (the last sync time, plus each submission's `dateLastActivity`). It advances only after a run fully succeeds, so a failed fetch never skips records on the next run.
223
+ - Submissions and extended submissions are pulled newest-first by `dateLastActivity` and stop early at the watermark. Publication details are skipped for submissions whose `dateLastActivity` is unchanged — the biggest saving, since that endpoint costs one request per submission.
224
+ - A one-day overlap buffer re-pulls the boundary on each run; merges are idempotent (upsert by id), so the overlap is harmless.
225
+ - View stats: `publication_stats` (cumulative totals) is always pulled in full, while the daily `views_timeline` is re-pulled over a rolling window and merged by `(submission_id, interval, date, kind)`, refreshing recent buckets without dropping history.
226
+ - Users are always pulled in full — the API exposes no recency sort for users.
227
+
228
+ The OJS API has no server-side "modified since" filter, so incremental cannot detect upstream deletions; run `ojs api fetch --full` periodically to reconcile.
229
+
230
+ ## Security & privacy
231
+
232
+ - The API token lives in `.env` (the `init` prompt hides input). `.env` is gitignored — keep it out of version control and out of shared locations.
233
+ - The API JSON dumps contain personal data pulled from OJS: `users.json` holds user records **including email addresses**, and the author/submission tables carry author names, emails, and ORCIDs. These files are written with the process umask (typically `0644`, i.e. world-readable). On a shared or multi-user host, run with a restrictive umask (e.g. `umask 077`) or point `OJS_DATA_DIR` at a private directory so other local users can't read them.
ojs-0.7.2/README.md ADDED
@@ -0,0 +1,218 @@
1
+ # ojs
2
+
3
+ Tools for working with the Open Journal Systems (OJS) API.
4
+
5
+ Pulls submissions, publications, reviews, users, and publication view statistics
6
+ from an OJS journal's `/api/v1/*` REST API, downloads the attached file artifacts
7
+ (manuscripts, revisions, reviewer attachments, and production galleys), and
8
+ normalizes the JSON into typed relational tables backed by polars. Incremental
9
+ sync re-pulls only what changed since the last run, so routine top-ups stay
10
+ cheap. A typed schema layer (`Column`/`Table` classes) is the single source of
11
+ truth for normalization and doubles as exportable column documentation. Also
12
+ normalizes the OJS dashboard's Articles and Reviews CSV report exports. Ships a
13
+ Typer CLI for the common fetch, download, and normalize workflows. Built against
14
+ the [OJS 3.3 REST API](https://docs.pkp.sfu.ca/dev/api/ojs/3.3); other versions
15
+ are untested and may differ, as the REST API saw breaking changes between 3.2
16
+ and 3.3.
17
+
18
+ ## Project Structure
19
+
20
+ ```
21
+ ojs/
22
+ ├── cli.py # Typer CLI: init, articles, reviews, api (+ schema docs)
23
+ ├── schema.py # Typed schema framework: Column/Table, apply(), doc export
24
+ ├── utils.py # HTML stripping + localized-field extraction
25
+ ├── website/ # Manual website CSV-export pipelines
26
+ │ ├── articles/ # Wide CSV → submissions, authors, editors, decisions
27
+ │ └── reviews/ # Long CSV → reviews
28
+ └── api/ # REST pipeline
29
+ ├── client.py # OJS REST client (httpx, pagination, retry, early-stop)
30
+ ├── files.py # Submission file artifact downloads (disk layout, manifest)
31
+ ├── normalize.py # JSON → relational tables (schema-driven)
32
+ ├── schemas.py # API table schema classes
33
+ ├── sync.py # Incremental sync: high-water-mark state, raw-JSON upsert
34
+ └── swagger.json # OJS API reference (snapshot)
35
+ ```
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ uv tool install ojs
41
+ ```
42
+
43
+ As a project dependency:
44
+
45
+ ```bash
46
+ uv add ojs
47
+ ```
48
+
49
+ From GitHub instead of PyPI:
50
+
51
+ ```bash
52
+ uv tool install git+https://github.com/gitronald/ojs.git
53
+ # or, as a dependency: uv add git+https://github.com/gitronald/ojs.git
54
+ ```
55
+
56
+ From source (for development):
57
+
58
+ ```bash
59
+ git clone https://github.com/gitronald/ojs.git
60
+ cd ojs
61
+ uv sync
62
+ ```
63
+
64
+ ## Configuration
65
+
66
+ The CLI reads from a `.env` file in the current directory. Run `ojs init` to
67
+ scaffold one — it prompts for the journal URL and API token, and writes `.env`
68
+ with `0600` permissions:
69
+
70
+ ```bash
71
+ ojs init
72
+ ```
73
+
74
+ **Getting an API key.** In OJS, open your user profile
75
+ (`https://example.org/index.php/myjournal/user/profile`), select the **API Key**
76
+ tab, check **Enable external applications with the API key to access this
77
+ account**, and copy the key — use the **(re)generate** button if one isn't set
78
+ yet.
79
+
80
+ Values can also come from the environment. A user-level config file is loaded as
81
+ a fallback for anything not set in the current directory's `.env` (which takes
82
+ precedence): `~/.config/ojs/.env` by default, or the file named by
83
+ `OJS_CONFIG_PATH`.
84
+
85
+ | Variable | Default | Purpose |
86
+ | --- | --- | --- |
87
+ | `OJS_BASE_URL` | (required for `api`) | OJS journal URL (e.g. `https://example.org/index.php/myjournal`) |
88
+ | `OJS_API_KEY` | (required for `api`) | OJS API token |
89
+ | `OJS_DATA_DIR` | `data/ojs-api` | Root for inputs and outputs |
90
+ | `OJS_DOWNLOADS_DIR` | `data/ojs-website` | Where CSV exports land |
91
+ | `OJS_ARTICLES_DIR` | `$OJS_DATA_DIR/articles` | Articles output dir |
92
+ | `OJS_REVIEWS_DIR` | `$OJS_DATA_DIR/reviews` | Reviews output dir |
93
+ | `OJS_API_DIR` | `$OJS_DATA_DIR` | API JSON dump dir |
94
+ | `OJS_FILES_DIR` | `$OJS_API_DIR/files` | Where downloaded submission files land |
95
+
96
+ ## CLI Commands
97
+
98
+ `norm` reads the typed schema classes directly — no separate step is required.
99
+ `schema` exports a `table_schemas.csv` documenting each table's columns, dtypes,
100
+ source mapping, and whether each column appears in the normalized output
101
+ (`in_output`).
102
+
103
+ ### API
104
+
105
+ Fetch raw JSON from the REST API, download file artifacts, and normalize into
106
+ relational tables.
107
+
108
+ ```bash
109
+ ojs api fetch # fetch raw JSON from the OJS REST API
110
+ ojs api download # download submission file artifacts (PDFs, etc.)
111
+ ojs api norm # normalize API JSON into relational tables
112
+ ojs api schema # export table_schemas.csv docs
113
+ ```
114
+
115
+ ### Articles
116
+
117
+ Normalize the OJS dashboard's Articles Report CSV export.
118
+
119
+ ```bash
120
+ ojs articles norm # normalize the most recent articles export
121
+ ojs articles schema # export table_schemas.csv docs
122
+ ```
123
+
124
+ ### Reviews
125
+
126
+ Normalize the OJS dashboard's Review Report CSV export.
127
+
128
+ ```bash
129
+ ojs reviews norm # normalize the most recent reviews export
130
+ ojs reviews schema # export table_schemas.csv docs
131
+ ```
132
+
133
+ ### Article view stats
134
+
135
+ `ojs api fetch` also pulls publication view stats from the OJS `/stats/publications/*`
136
+ endpoints (skip with `--no-stats`). The API only exposes aggregated counts — the
137
+ finest granularity is **daily** (there are no per-event timestamps).
138
+
139
+ | Flag | Default | Purpose |
140
+ | --- | --- | --- |
141
+ | `--stats / --no-stats` | on | Toggle stats collection (e.g. when the API key lacks stats access) |
142
+ | `--stats-interval` | `day` | Timeline granularity: `day` or `month` |
143
+ | `--stats-since` | (none) | `dateStart` filter (`YYYY-MM-DD`) |
144
+ | `--stats-until` | (none) | `dateEnd` filter (`YYYY-MM-DD`) |
145
+
146
+ `ojs api norm` then writes three extra tables:
147
+
148
+ - `publication_stats` — one row per published submission with abstract, all-galley, PDF, HTML, and other view totals.
149
+ - `views_timeline` — long format (`submission_id`, `date`, `interval`, `views`, `kind`) with a per-submission abstract and galley series. `interval` records the granularity (`day` or `month`) a point was fetched at, so a file mixing both stays separable — filter on it rather than summing across intervals.
150
+ - `views_timeline_totals` — long format (`date`, `interval`, `views`, `kind`) with the journal-wide abstract and galley series, from the aggregate `/stats/publications/{abstract,galley}` endpoints (the data behind the OJS statistics-page graph). Use this for journal-wide totals rather than summing `views_timeline`.
151
+
152
+ If the API key lacks stats access, `fetch` prints a warning and skips the stats files, and `norm` simply omits the two tables.
153
+
154
+ ### Submission files
155
+
156
+ OJS attaches the actual file artifacts (manuscripts, revisions, reviewer
157
+ attachments, production galleys) to each submission. `ojs api download` fetches
158
+ their metadata and then downloads the binaries.
159
+
160
+ ```bash
161
+ ojs api fetch --files # also dump file metadata -> submission_files.json
162
+ ojs api download # download all files for all submissions
163
+ ojs api download -s 123 -s 456 # only these submissions (repeatable)
164
+ ojs api download --type galleys # only published galley files
165
+ ojs api download --type review # only review files / revisions / attachments
166
+ ojs api download --file-stage 4 --file-stage 15 # raw fileStage ids
167
+ ojs api download --no-revisions # current files only, skip prior revisions
168
+ ```
169
+
170
+ | Flag | Default | Purpose |
171
+ | --- | --- | --- |
172
+ | `--submission-id` / `-s` | all | Limit to these submission ids (repeatable) |
173
+ | `--type` | `all` | `all`, `galleys` (published), or `review` |
174
+ | `--file-stage` | (none) | Raw `fileStage` id(s); overrides `--type` |
175
+ | `--revisions / --no-revisions` | on | Also download prior revisions of each file |
176
+ | `--fetch / --no-fetch` | on | Refresh file metadata first (off: use stored JSON) |
177
+
178
+ Files are laid out under `OJS_FILES_DIR` as
179
+ `<submission_id>/<stage>/<fileId>_<name>`. A manifest (`manifest.json`) records
180
+ every artifact by its immutable physical `fileId`, so reruns skip files already
181
+ on disk — new uploads and revisions are downloaded incrementally.
182
+
183
+ **Rounds and revisions.** OJS tracks two distinct axes. A file's *stage*
184
+ (`fileStage`) says where in the workflow it lives; review files additionally
185
+ carry an `assocId` naming the **review round** they belong to. Separately, each
186
+ file's `revisions[]` holds prior uploads of that same logical file. `ojs api
187
+ norm` writes a `submission_files` table with one row per current file, including
188
+ `file_stage_label`, `review_round_id` (joins to `review_assignments.round_id`),
189
+ and `revision_count`. Downloads cover the current file plus every revision, each
190
+ keyed by its own `fileId`.
191
+
192
+ Downloading files requires an API token with permission to view them; the API
193
+ returns `403` for files the key cannot access.
194
+
195
+ ### Incremental fetch
196
+
197
+ By default `ojs api fetch` does a full cold pull. For routine top-ups, `--incremental` fetches only what changed since the last successful sync and merges it into the existing JSON dumps, so `ojs api norm` stays a stateless re-derivation from the complete files.
198
+
199
+ | Flag | Purpose |
200
+ | --- | --- |
201
+ | `--incremental` / `-i` | Fetch only records changed since the last sync, merging into the JSON dumps |
202
+ | `--since YYYY-MM-DD` | Override the stored watermark (implies `--incremental`) |
203
+ | `--full` | Force a complete pull and reset the sync state |
204
+
205
+ How it works:
206
+
207
+ - A high-water mark lives in `data/ojs-api/sync_state.json` (the last sync time, plus each submission's `dateLastActivity`). It advances only after a run fully succeeds, so a failed fetch never skips records on the next run.
208
+ - Submissions and extended submissions are pulled newest-first by `dateLastActivity` and stop early at the watermark. Publication details are skipped for submissions whose `dateLastActivity` is unchanged — the biggest saving, since that endpoint costs one request per submission.
209
+ - A one-day overlap buffer re-pulls the boundary on each run; merges are idempotent (upsert by id), so the overlap is harmless.
210
+ - View stats: `publication_stats` (cumulative totals) is always pulled in full, while the daily `views_timeline` is re-pulled over a rolling window and merged by `(submission_id, interval, date, kind)`, refreshing recent buckets without dropping history.
211
+ - Users are always pulled in full — the API exposes no recency sort for users.
212
+
213
+ The OJS API has no server-side "modified since" filter, so incremental cannot detect upstream deletions; run `ojs api fetch --full` periodically to reconcile.
214
+
215
+ ## Security & privacy
216
+
217
+ - The API token lives in `.env` (the `init` prompt hides input). `.env` is gitignored — keep it out of version control and out of shared locations.
218
+ - The API JSON dumps contain personal data pulled from OJS: `users.json` holds user records **including email addresses**, and the author/submission tables carry author names, emails, and ORCIDs. These files are written with the process umask (typically `0644`, i.e. world-readable). On a shared or multi-user host, run with a restrictive umask (e.g. `umask 077`) or point `OJS_DATA_DIR` at a private directory so other local users can't read them.
@@ -0,0 +1 @@
1
+ __version__ = "0.7.2"
File without changes