eco-helpers 3.2.13 → 3.2.16
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.
- checksums.yaml +4 -4
- data/.ai-assistance/conventions/code-working-tree-protocol.md +176 -0
- data/.ai-assistance/scripts/token-logger.js +220 -0
- data/.ai-assistance/scripts/token-report.ts +158 -0
- data/.ai-assistance/scripts/token-session-start.js +66 -0
- data/.ai-assistance/skills/ep-ai-manager/SKILL.md +417 -0
- data/.ai-assistance/skills/ruby-scripting/SKILL.md +215 -0
- data/.ai-assistance/standards-version.json +10 -0
- data/.ai-assistance/token-budget.json +39 -0
- data/.claude/settings.json +103 -0
- data/.gitignore +2 -0
- data/CHANGELOG.md +29 -1
- data/CLAUDE.md +83 -0
- data/eco-helpers.gemspec +1 -1
- data/lib/eco/api/usecases/CLAUDE.md +78 -0
- data/lib/eco/api/usecases/default/pages.rb +30 -0
- data/lib/eco/api/usecases/default/utils/add_page_id_case.rb +273 -0
- data/lib/eco/api/usecases/default/utils/cli/add_page_id_cli.rb +29 -0
- data/lib/eco/api/usecases/default/utils/cli/group_csv_cli.rb +5 -0
- data/lib/eco/api/usecases/default/utils/cli/track_files_cli.rb +16 -0
- data/lib/eco/api/usecases/default/utils/group_csv_case/file_handler.rb +62 -0
- data/lib/eco/api/usecases/default/utils/group_csv_case.rb +64 -22
- data/lib/eco/api/usecases/default/utils/track_files_case.rb +179 -0
- data/lib/eco/api/usecases/default/utils.rb +2 -0
- data/lib/eco/api/usecases/graphql/CLAUDE.md +120 -0
- data/lib/eco/api/usecases/graphql/compat/ooze_redirect/dirty_array.rb +22 -0
- data/lib/eco/api/usecases/graphql/compat/ooze_redirect/field_patches.rb +241 -0
- data/lib/eco/api/usecases/graphql/compat/ooze_redirect/force_compat.rb +73 -0
- data/lib/eco/api/usecases/graphql/compat/ooze_redirect.rb +234 -0
- data/lib/eco/api/usecases/graphql/compat.rb +6 -0
- data/lib/eco/api/usecases/graphql/helpers/CLAUDE.md +79 -0
- data/lib/eco/api/usecases/graphql/samples/CLAUDE.md +76 -0
- data/lib/eco/api/usecases/graphql/samples/pages/CLAUDE.md +59 -0
- data/lib/eco/api/usecases/graphql/samples/pages/org_page/base.rb +41 -0
- data/lib/eco/api/usecases/graphql/samples/pages/org_page/dsl.rb +8 -0
- data/lib/eco/api/usecases/graphql/samples/pages/org_page.rb +7 -0
- data/lib/eco/api/usecases/graphql/samples/pages/page/base.rb +148 -0
- data/lib/eco/api/usecases/graphql/samples/pages/page/dsl.rb +38 -0
- data/lib/eco/api/usecases/graphql/samples/pages/page.rb +7 -0
- data/lib/eco/api/usecases/graphql/samples/pages.rb +7 -0
- data/lib/eco/api/usecases/graphql/samples.rb +1 -0
- data/lib/eco/api/usecases/graphql.rb +1 -0
- data/lib/eco/api/usecases/ooze_samples/ooze_base_case.rb +4 -0
- data/lib/eco/api/usecases/ooze_samples/register_update_case.rb +7 -1
- data/lib/eco/version.rb +1 -1
- metadata +36 -3
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"defaultMode": "auto",
|
|
4
|
+
"allowedTools": [
|
|
5
|
+
"Read"
|
|
6
|
+
],
|
|
7
|
+
"permissionRules": [
|
|
8
|
+
{
|
|
9
|
+
"tool": "Write",
|
|
10
|
+
"pattern": "/**",
|
|
11
|
+
"action": "allow"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"tool": "StrReplace",
|
|
15
|
+
"pattern": "/**",
|
|
16
|
+
"action": "allow"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"allow": [
|
|
20
|
+
"WebFetch(domain:anthropic.com)",
|
|
21
|
+
"WebFetch(domain:npmjs.com)",
|
|
22
|
+
"Write(.glaudeignore)",
|
|
23
|
+
"Update(.glaudeignore)",
|
|
24
|
+
"Bash(rm .ai-assistance/bridge/LOCK)",
|
|
25
|
+
"Read(.git/**)",
|
|
26
|
+
"Read(**/rubygems/**)",
|
|
27
|
+
"Write(*.md)",
|
|
28
|
+
"Edit(.ai-assistance/**)",
|
|
29
|
+
"Update(.ai-assistance/**)",
|
|
30
|
+
"Write(.ai-assistance/**)",
|
|
31
|
+
"Write(lib/**)",
|
|
32
|
+
"Write(spec/**)",
|
|
33
|
+
"Bash(git status)",
|
|
34
|
+
"Bash(git diff *)",
|
|
35
|
+
"Bash(git log *)",
|
|
36
|
+
"Bash(git add *)",
|
|
37
|
+
"Bash(git commit *)",
|
|
38
|
+
"PowerShell(git status)",
|
|
39
|
+
"PowerShell(git diff *)",
|
|
40
|
+
"PowerShell(git log *)",
|
|
41
|
+
"PowerShell(git add *)",
|
|
42
|
+
"PowerShell(git commit *)",
|
|
43
|
+
"Bash(npm test)",
|
|
44
|
+
"Bash(npm run lint)",
|
|
45
|
+
"Bash(npm run build)",
|
|
46
|
+
"Bash(vitest *)",
|
|
47
|
+
"Bash(jest *)",
|
|
48
|
+
"PowerShell(npm test)",
|
|
49
|
+
"PowerShell(npm run lint)",
|
|
50
|
+
"PowerShell(npm run build)",
|
|
51
|
+
"Bash(black .)",
|
|
52
|
+
"Bash(pytest)",
|
|
53
|
+
"Bash(python -m unittest)",
|
|
54
|
+
"Bash(ruff check *)",
|
|
55
|
+
"Bash(bundle exec rspec *)",
|
|
56
|
+
"Bash(bundle exec rubocop *)",
|
|
57
|
+
"PowerShell(pytest)",
|
|
58
|
+
"PowerShell(python -m unittest)",
|
|
59
|
+
"PowerShell(ruff check *)",
|
|
60
|
+
"PowerShell(bundle exec rspec *)",
|
|
61
|
+
"PowerShell(bundle exec rubocop *)",
|
|
62
|
+
"Bash(node .ai-assistance/scripts/token-logger.js)",
|
|
63
|
+
"Bash(node .ai-assistance/scripts/token-session-start.js)"
|
|
64
|
+
],
|
|
65
|
+
"deny": [
|
|
66
|
+
"Read(*.env)",
|
|
67
|
+
"Read(./.env*)",
|
|
68
|
+
"Read(./secrets/**)",
|
|
69
|
+
"Bash(*cat *.env*)",
|
|
70
|
+
"Bash(*grep *.env*)",
|
|
71
|
+
"Bash(printenv*)",
|
|
72
|
+
"Bash(env)",
|
|
73
|
+
"Write(.git/*)",
|
|
74
|
+
"Edit(.git/*)",
|
|
75
|
+
"Bash(git push *)",
|
|
76
|
+
"Bash(rm -rf *)"
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
"hooks": {
|
|
80
|
+
"SessionStart": [
|
|
81
|
+
{
|
|
82
|
+
"matcher": "",
|
|
83
|
+
"hooks": [
|
|
84
|
+
{
|
|
85
|
+
"type": "command",
|
|
86
|
+
"command": "node .ai-assistance/scripts/token-session-start.js 2>/dev/null || true"
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
"Stop": [
|
|
92
|
+
{
|
|
93
|
+
"matcher": "",
|
|
94
|
+
"hooks": [
|
|
95
|
+
{
|
|
96
|
+
"type": "command",
|
|
97
|
+
"command": "node .ai-assistance/scripts/token-logger.js 2>/dev/null || true"
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
}
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [3.2.
|
|
5
|
+
## [3.2.16] - 2026-07-02
|
|
6
|
+
|
|
7
|
+
APIv2→GraphQL cutover — the OozeRedirect compat layer that runs the maintained ooze integrations
|
|
8
|
+
(toocs-coding, cans-upsert, supplier-documents) on GraphQL. Requires ecoportal-api-graphql >= 1.3.9.
|
|
9
|
+
|
|
10
|
+
### Fixed / Changed
|
|
11
|
+
|
|
12
|
+
- **OozeRedirect slimmed onto the base OozeSamples loop** — removed the `with_each_entry` /
|
|
13
|
+
`update_ooze` / `process_ooze` overrides that duplicated and silently bypassed the base loop
|
|
14
|
+
(KPIs, dedup, queue, `dry_run_feedback`). Fetches go through `ooze → apiv2.pages.get` and saves
|
|
15
|
+
through `update_oozes → update_ooze → apiv2.pages.update`; a captured `submit!`/`sign_off!` rides
|
|
16
|
+
along on the single `updatePage` via `Input::Page::Update.from_model` (reads the `_compat_*` flags).
|
|
17
|
+
- **`dirty?`** also treats a pending `submit!`/`sign_off!` as dirty (so a submit-only page still saves).
|
|
18
|
+
- **`OozeBaseCase#dry_run_feedback`** prints an affirmative `[dry-run] would create/update <ref>` line.
|
|
19
|
+
- **`RegisterUpdateCase#enqueue`** accepts any duck-typed entry (`#dirty?` + `#as_update`), not just
|
|
20
|
+
v2 `Page`/`PageStage` — GraphQL compat pages queue correctly under OozeRedirect.
|
|
21
|
+
|
|
22
|
+
## [3.2.15] - 2026-05-xx
|
|
6
23
|
|
|
7
24
|
### Added
|
|
8
25
|
|
|
@@ -10,6 +27,17 @@ All notable changes to this project will be documented in this file.
|
|
|
10
27
|
|
|
11
28
|
### Fixed
|
|
12
29
|
|
|
30
|
+
## [3.2.14] - 2026-05-22
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- `track-files` case
|
|
35
|
+
- `add-page-id` case
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- **improvement**: added `-format` argument to `-group-csv` to output a `jsonl` **custom** file.
|
|
40
|
+
|
|
13
41
|
## [3.2.13] - 2026-04-15
|
|
14
42
|
|
|
15
43
|
### Added
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# CLAUDE.md — eco-helpers
|
|
2
|
+
|
|
3
|
+
AI agent instructions for this repository.
|
|
4
|
+
|
|
5
|
+
**Cross-cutting architecture context lives in `ecoportal-api-graphql` — see its `CLAUDE.md` and `.claude/` folder for the full dependency map, project history, and shared skills.**
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Repository Role
|
|
10
|
+
|
|
11
|
+
`eco-helpers` is the **primary downstream consumer** of the EcoPortal API gem stack. It provides a scripting and automation framework for interacting with EcoPortal — CLI tooling, use-case orchestration, data transformation, and batch operations.
|
|
12
|
+
|
|
13
|
+
**Position in chain:**
|
|
14
|
+
```
|
|
15
|
+
ecoportal-api
|
|
16
|
+
ecoportal-api-v2
|
|
17
|
+
ecoportal-api-graphql
|
|
18
|
+
↓
|
|
19
|
+
eco-helpers ← THIS REPO
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Remote:** https://gitlab.ecoportal.co.nz/oscar/script_api_helpers.git
|
|
23
|
+
|
|
24
|
+
**Gem dependencies on the stack:**
|
|
25
|
+
- `ecoportal-api ~> 0.10, >= 0.10.14`
|
|
26
|
+
- `ecoportal-api-v2 ~> 3.3, >= 3.3.1`
|
|
27
|
+
- `ecoportal-api-graphql ~> 1.3, >= 1.3.4`
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Key Folder Layout
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
lib/eco/
|
|
35
|
+
api/ API integration layer
|
|
36
|
+
common/ Shared helpers
|
|
37
|
+
microcases/ Fine-grained reusable operations
|
|
38
|
+
organization/ Org-level resources
|
|
39
|
+
session.rb Session management (entry point for scripting)
|
|
40
|
+
usecases.rb Use-case registry
|
|
41
|
+
policies.rb Access policies
|
|
42
|
+
cli/ CLI framework
|
|
43
|
+
cli_default/ Default CLI options, filters, people workflows
|
|
44
|
+
common/ Cross-cutting utilities
|
|
45
|
+
csv/ CSV reading, streaming, splitting
|
|
46
|
+
data/ Data utilities (fuzzy match, hashes, locations, strings, files)
|
|
47
|
+
language/ Logging, curry, auxiliar utilities
|
|
48
|
+
assets/ Static assets (language files etc.)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Namespace
|
|
54
|
+
|
|
55
|
+
`Eco::` — entirely separate namespace from `Ecoportal::`. Does not reopen upstream gem namespaces.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Key Concerns
|
|
60
|
+
|
|
61
|
+
- This gem is the **backwards-compatibility target** for all upstream gems. When `ecoportal-api-graphql` changes its public interface, check usage here first.
|
|
62
|
+
- `Eco::API::Session` is the main consumer of `Ecoportal::API::GraphQL` — it's the first place to look when checking how GraphQL features are used downstream.
|
|
63
|
+
- The CLI layer (`Eco::CLI`) uses Thor-style commands — changes to API interfaces may silently break CLI workflows if not tested end-to-end.
|
|
64
|
+
- Many operations are batch-oriented with progress logging — error handling and partial-failure behaviour matters.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## How to Find GraphQL Usage
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
grep -r "GraphQL\|graphql\|ecoportal-api-graphql" lib/ --include="*.rb" -l
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This shows which files directly use the GraphQL gem — useful when assessing impact of upstream changes.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Running Tests
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
bundle install
|
|
82
|
+
bundle exec rspec
|
|
83
|
+
```
|
data/eco-helpers.gemspec
CHANGED
|
@@ -42,7 +42,7 @@ Gem::Specification.new do |spec|
|
|
|
42
42
|
spec.add_dependency 'docx', '>= 0.8.0', '< 0.9'
|
|
43
43
|
spec.add_dependency 'dotenv', '~> 3'
|
|
44
44
|
spec.add_dependency 'ecoportal-api', '~> 0.10', '>= 0.10.14'
|
|
45
|
-
spec.add_dependency 'ecoportal-api-graphql', '~> 1.3', '>= 1.3.
|
|
45
|
+
spec.add_dependency 'ecoportal-api-graphql', '~> 1.3', '>= 1.3.9'
|
|
46
46
|
spec.add_dependency 'ecoportal-api-v2', '~> 3.3', '>= 3.3.1'
|
|
47
47
|
spec.add_dependency 'ed25519', '~> 1.2'
|
|
48
48
|
spec.add_dependency 'fast_excel', '>= 0.5.0', '< 0.6'
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# usecases
|
|
2
|
+
|
|
3
|
+
The use-case registry and all built-in case base classes for scripting against EcoPortal.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## What a use case is
|
|
8
|
+
|
|
9
|
+
A use case is a self-contained, named, runnable unit of work. It registers itself with
|
|
10
|
+
the CLI framework, receives `session`, `options`, and `usecase` from the runner, and
|
|
11
|
+
executes its `process` (or `process_ooze` / `process_page`) method.
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
CLI invokes rake → rake finds registered case → UseCase#launch → main() → process()
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Directory structure
|
|
20
|
+
|
|
21
|
+
| Path | What lives there |
|
|
22
|
+
|------|-----------------|
|
|
23
|
+
| `graphql/` | GraphQL-native base cases + samples (see `graphql/CLAUDE.md`) |
|
|
24
|
+
| `ooze_samples/` | APIv2/REST base cases: `OozeBaseCase`, `RegisterUpdateCase` |
|
|
25
|
+
| `ooze_cases/` | Concrete built-in ooze cases (export register, etc.) |
|
|
26
|
+
| `default/` | Built-in people, location, and utility cases |
|
|
27
|
+
| `default_cases/` | Loader and samples for default cases |
|
|
28
|
+
| `samples/` | Misc driver samples |
|
|
29
|
+
| `graphql.rb` | GraphQL use case loader |
|
|
30
|
+
| `ooze_samples.rb` | Ooze/REST use case loader |
|
|
31
|
+
| `default.rb` | Default use case loader |
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Adding a new use case
|
|
36
|
+
|
|
37
|
+
1. Subclass the appropriate base:
|
|
38
|
+
|
|
39
|
+
| Your use case | Inherit from |
|
|
40
|
+
|---|---|
|
|
41
|
+
| Process pages in a register (update workflow) | `Eco::API::UseCases::GraphQL::PageCase` |
|
|
42
|
+
| Process pages org-wide (cross-register, audit) | `Eco::API::UseCases::GraphQL::OrgPageCase` |
|
|
43
|
+
| Custom GraphQL script (export, report, one-off) | `Eco::API::UseCases::GraphQL::Base` |
|
|
44
|
+
| Legacy APIv2 register update | `Eco::API::UseCases::OozeSamples::RegisterUpdateCase` |
|
|
45
|
+
|
|
46
|
+
2. Set `name` and `type`:
|
|
47
|
+
```ruby
|
|
48
|
+
name 'my-case-name' # CLI identifier: called with -my-case-name
|
|
49
|
+
type :other # :people | :contractors | :other
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
3. Override the entry point (`process_page`, `process`, or `process_ooze`).
|
|
53
|
+
|
|
54
|
+
4. Register in the org's `config/cli.rb`:
|
|
55
|
+
```ruby
|
|
56
|
+
cases.add('-my-case-name', :other, 'Description')
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## How cases are launched
|
|
62
|
+
|
|
63
|
+
`Eco::API::UseCases::UseCase#launch` calls `callback.call(*uio.params)` where the
|
|
64
|
+
callback is bound to `method(:main)`. Before launch, `@session` and `@options` are
|
|
65
|
+
injected into the instance — subclasses access them via the `attr_reader` in `CaseEnv`.
|
|
66
|
+
|
|
67
|
+
The `:other` type passes `(session, options, usecase)` positionally to `main`.
|
|
68
|
+
For `GraphQL::Base` subclasses the signature is `main(*_args)` — `session` and
|
|
69
|
+
`options` are already available via the helpers module before `main` is called.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Related
|
|
74
|
+
|
|
75
|
+
- `graphql/CLAUDE.md` — GraphQL case hierarchy, PageCase/OrgPageCase
|
|
76
|
+
- `ooze_samples/` — legacy v2 cases (RegisterUpdateCase, OozeBaseCase)
|
|
77
|
+
- `eco-helpers/CLAUDE.md` — top-level gem context
|
|
78
|
+
- `ecoportal-api-graphql` — upstream gem providing `SearchConf`, `Compat::Pages`, etc.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Eco
|
|
2
|
+
module API
|
|
3
|
+
class UseCases
|
|
4
|
+
class Default
|
|
5
|
+
# Namespace for CLI-integrated page use cases.
|
|
6
|
+
# These are concrete, fully functional cases exposed to all org environments.
|
|
7
|
+
#
|
|
8
|
+
# == Convention
|
|
9
|
+
# Cases here should:
|
|
10
|
+
# - Inherit from Eco::API::UseCases::GraphQL::Samples::Pages::Page::Base
|
|
11
|
+
# (or OrgPage::Base for org-wide operations)
|
|
12
|
+
# - Be registered in the org's config/cli.rb
|
|
13
|
+
# - Be as org-agnostic as possible (accept register_id, filters via options)
|
|
14
|
+
#
|
|
15
|
+
# == Adding a new default page case
|
|
16
|
+
# 1. Create the file here: default/pages/my_case.rb
|
|
17
|
+
# 2. Inherit from the appropriate pages sample base class
|
|
18
|
+
# 3. Require it below
|
|
19
|
+
# 4. Wire CLI registration in usecases/default_cases/samples.rb or the
|
|
20
|
+
# org's config/cli.rb
|
|
21
|
+
#
|
|
22
|
+
# == Currently
|
|
23
|
+
# No default page cases yet — add them here as common patterns emerge
|
|
24
|
+
# across org implementations.
|
|
25
|
+
module Pages
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# @note you might add a `filter` method
|
|
2
|
+
#
|
|
3
|
+
# def filter
|
|
4
|
+
# @filter ||= proc do |row, _r_idx|
|
|
5
|
+
# next true
|
|
6
|
+
# next true unless (ref_id = row[pivot_column(row)])
|
|
7
|
+
# next false if excluded_ref_id?(ref_id)
|
|
8
|
+
#
|
|
9
|
+
# true
|
|
10
|
+
# end
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
class Eco::API::UseCases::Default::Utils::AddPageId < Eco::API::Custom::UseCase
|
|
14
|
+
name 'add-page-id'
|
|
15
|
+
type :other
|
|
16
|
+
|
|
17
|
+
require_relative 'cli/add_page_id_cli'
|
|
18
|
+
|
|
19
|
+
PIVOT_FIELD = [
|
|
20
|
+
'ref_id'
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
PAGE_ID = 'page_id'.freeze
|
|
24
|
+
EXCLUDED_REF_IDS = %w[].freeze
|
|
25
|
+
|
|
26
|
+
def main(*_args)
|
|
27
|
+
if simulate?
|
|
28
|
+
count = Eco::CSV.count(input_file)
|
|
29
|
+
log(:info) { "CSV '#{input_file}' has #{count} rows." }
|
|
30
|
+
else
|
|
31
|
+
generate_file(&filter)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
attr_reader :headers, :headers_rest
|
|
38
|
+
|
|
39
|
+
def filter
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def excluded_ref_id?(ref_id)
|
|
44
|
+
self.class::EXCLUDED_REF_IDS.include?(ref_id)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def generate_file # rubocop:disable Metrics/AbcSize
|
|
48
|
+
idx = -1
|
|
49
|
+
row_count = 0
|
|
50
|
+
headers_added = false
|
|
51
|
+
|
|
52
|
+
CSV.open(output_filename, 'wb') do |csv|
|
|
53
|
+
puts "\n"
|
|
54
|
+
|
|
55
|
+
Eco::CSV.foreach(input_file, headers: true, skip_blanks: true) do |row|
|
|
56
|
+
idx += 1
|
|
57
|
+
|
|
58
|
+
next unless !block_given? || yield(row, idx)
|
|
59
|
+
|
|
60
|
+
unless headers_added
|
|
61
|
+
headers!(row)
|
|
62
|
+
require_pivot_field!(row, file: input_file)
|
|
63
|
+
|
|
64
|
+
csv << headers
|
|
65
|
+
headers_added = true
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
unless (pivot_value = row[pivot_field])
|
|
69
|
+
msg = "Row #{idx} doesn't have value for pivot field '#{pivot_field}'"
|
|
70
|
+
msg << ". Skipping (discarded) ..."
|
|
71
|
+
log(:warn) { msg }
|
|
72
|
+
next
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
unless (page_id = input_maps[pivot_value])
|
|
76
|
+
warn_unknown_mapping_reference!(pivot_value)
|
|
77
|
+
next
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
row_count += 1
|
|
81
|
+
|
|
82
|
+
if (row_count % 500).zero?
|
|
83
|
+
print "... Mapped #{row_count} rows \r"
|
|
84
|
+
$stdout.flush
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
values = [page_id, pivot_value]
|
|
88
|
+
oth_values = row.values_at(*headers_rest)
|
|
89
|
+
values.concat(oth_values) unless headers_rest.empty?
|
|
90
|
+
|
|
91
|
+
csv << values
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
ensure
|
|
95
|
+
msg = "Generated file '#{output_filename}' with #{row_count} rows (out of #{idx})."
|
|
96
|
+
log(:info) { msg } unless simulate?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def warn_unknown_mapping_reference!(ref_id)
|
|
100
|
+
return if unknown.include?(ref_id)
|
|
101
|
+
|
|
102
|
+
unknown << ref_id
|
|
103
|
+
msg = "Could not map '#{pivot_field}' '#{ref_id}' to a '#{page_id_field}'"
|
|
104
|
+
msg << ". Skipping (discarded) ..."
|
|
105
|
+
|
|
106
|
+
log(:warn) { msg }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def unknown
|
|
110
|
+
@unknown ||= []
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def headers!(row)
|
|
114
|
+
return if instance_variable_defined?(:@headers)
|
|
115
|
+
|
|
116
|
+
@headers_rest = row.headers - base_out_header(row)
|
|
117
|
+
@headers = [*base_out_header, *headers_rest]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def base_out_header(row = nil)
|
|
121
|
+
@base_out_header ||= [page_id_field, pivot_field(row)] # space: :output
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def input_maps
|
|
125
|
+
return @input_maps if instance_variable_defined?(:@input_maps)
|
|
126
|
+
|
|
127
|
+
@input_maps = {}
|
|
128
|
+
idx = 0
|
|
129
|
+
|
|
130
|
+
Eco::CSV.foreach(input_maps_file, headers: true) do |row|
|
|
131
|
+
idx += 1
|
|
132
|
+
|
|
133
|
+
if (idx % 500).zero?
|
|
134
|
+
print "... Creating mappings table (#{idx} done) \r"
|
|
135
|
+
$stdout.flush
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
require_pivot_field!(row, space: :maps, file: input_maps_file)
|
|
139
|
+
require_page_id_field!(row, file: input_maps_file)
|
|
140
|
+
|
|
141
|
+
ref_id = row[pivot_field(space: :maps)]
|
|
142
|
+
page_id = row[page_id_field(space: :maps)]
|
|
143
|
+
|
|
144
|
+
@input_maps[ref_id] = page_id
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
@input_maps
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def input_maps_file
|
|
151
|
+
options.dig(:input, :maps).tap do |file|
|
|
152
|
+
next if file && File.exist?(file)
|
|
153
|
+
|
|
154
|
+
log(:error) {
|
|
155
|
+
msg = "You must specify an existing maps file with the option '-maps-file'"
|
|
156
|
+
msg << ".\n * File: '#{file}' does not exist" unless file.nil?
|
|
157
|
+
msg
|
|
158
|
+
}
|
|
159
|
+
exit 1
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def output_filename
|
|
164
|
+
return nil unless input_name
|
|
165
|
+
|
|
166
|
+
File.join(
|
|
167
|
+
input_dir,
|
|
168
|
+
"#{input_name}_mapped#{input_ext}"
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def input_name
|
|
173
|
+
@input_name ||= File.basename(
|
|
174
|
+
input_basename,
|
|
175
|
+
input_ext
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def input_ext
|
|
180
|
+
@input_ext ||= input_basename.split('.')[1..].join('.').then do |name|
|
|
181
|
+
".#{name}"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def input_basename
|
|
186
|
+
@input_basename ||= File.basename(input_full_filename)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def input_dir
|
|
190
|
+
@input_dir = File.dirname(input_full_filename)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def input_full_filename
|
|
194
|
+
@input_full_filename ||= File.expand_path(input_file)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def input_file
|
|
198
|
+
options.dig(:input, :file)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def require_pivot_field!(row, file:, space: :input)
|
|
202
|
+
return true if row.key?(pivot_field(row, space: space))
|
|
203
|
+
|
|
204
|
+
msg = "Pivot field '#{pivot_field}' missing in header of file '#{file}'"
|
|
205
|
+
log(:error) { msg }
|
|
206
|
+
raise msg
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def require_page_id_field!(row, file:)
|
|
210
|
+
return true if row.key?(page_id_field(space: :maps))
|
|
211
|
+
|
|
212
|
+
msg = "Page ID field '#{page_id_field(space: :maps)}' missing in header of file '#{file}'"
|
|
213
|
+
log(:error) { msg }
|
|
214
|
+
raise msg
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def pivot_field(row = nil, space: :input)
|
|
218
|
+
@pivot_field ||= {}
|
|
219
|
+
return @pivot_field[space] if @pivot_field.key?(space)
|
|
220
|
+
|
|
221
|
+
@pivot_field[space] ||= pivot_fields(space: space).select do |name|
|
|
222
|
+
row.key?(name)
|
|
223
|
+
end.then do |sel|
|
|
224
|
+
next sel.first if sel.one?
|
|
225
|
+
|
|
226
|
+
msg = "Could not find any column named: #{pivot_fields.join(', ')}"
|
|
227
|
+
msg = "Multiple pivot columns: #{sel.join(', ')}" if sel.any?
|
|
228
|
+
|
|
229
|
+
log(:error) { msg }
|
|
230
|
+
raise msg
|
|
231
|
+
end.tap do |col|
|
|
232
|
+
log(:info) { "Using header '#{col}' as pivot column." }
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def pivot_fields(space: :input)
|
|
237
|
+
@pivot_fields ||= {}
|
|
238
|
+
return @pivot_fields[space] if @pivot_fields.key?(space)
|
|
239
|
+
|
|
240
|
+
return (@pivot_fields[space] = [opts_pivot]) if opts_pivot && space == :input
|
|
241
|
+
|
|
242
|
+
unless self.class.const_defined?(:PIVOT_FIELD)
|
|
243
|
+
msg = "(#{self.class}) You must define PIVOT_FIELD constant"
|
|
244
|
+
log(:error) { msg }
|
|
245
|
+
raise msg
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
@pivot_fields[space] = self.class::PIVOT_FIELD.dup
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def page_id_field(space: :output)
|
|
252
|
+
@page_id_field = {}
|
|
253
|
+
return @page_id_field[space] if @page_id_field.key?(space)
|
|
254
|
+
|
|
255
|
+
return (@page_id_field[space] = opts_page_id) if opts_page_id && space == :output
|
|
256
|
+
|
|
257
|
+
unless self.class.const_defined?(:PAGE_ID)
|
|
258
|
+
msg = "(#{self.class}) You must define PAGE_ID field constant"
|
|
259
|
+
log(:error) { msg }
|
|
260
|
+
raise msg
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
@page_id_field[space] = self.class::PAGE_ID
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def opts_pivot
|
|
267
|
+
options.dig(:input, :pivot_field)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def opts_page_id
|
|
271
|
+
options.dig(:input, :page_id)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
class Eco::API::UseCases::Default::Utils::AddPageId
|
|
2
|
+
class Cli < Eco::API::UseCases::Cli
|
|
3
|
+
desc 'Adds the page_id column based on mappings onto -pivot'
|
|
4
|
+
|
|
5
|
+
callback do |_session, options, _usecase|
|
|
6
|
+
if (file = SCR.get_file(cli_name, required: true, should_exist: true))
|
|
7
|
+
options.deep_merge!(input: {file: file})
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
add_option('-maps-file', 'Source file with he mappings') do |options|
|
|
12
|
+
if (file = SCR.get_file('-maps-file', required: true, should_exist: true))
|
|
13
|
+
options.deep_merge!(input: {maps: file})
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_option('-pivot', 'The column that should be used to pivot') do |options|
|
|
18
|
+
if (file = SCR.get_arg("-pivot", with_param: true))
|
|
19
|
+
options.deep_merge!(input: {pivot_field: file})
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
add_option('-page-id', 'The column that should be used to dump the id') do |options|
|
|
24
|
+
if (file = SCR.get_arg("-page-id", with_param: true))
|
|
25
|
+
options.deep_merge!(input: {page_id: file})
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -22,5 +22,10 @@ class Eco::API::UseCases::Default::Utils::GroupCsv
|
|
|
22
22
|
options.deep_merge!(input: {group_by_field: file})
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
|
+
|
|
26
|
+
add_option('-format', 'Kind of extract (csv - default | jsonl') do |options|
|
|
27
|
+
format = SCR.get_arg('-format', with_param: true)
|
|
28
|
+
options.deep_merge!(output: {format: format})
|
|
29
|
+
end
|
|
25
30
|
end
|
|
26
31
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class Eco::API::UseCases::Default::Utils::TrackFiles
|
|
2
|
+
class Cli < Eco::API::UseCases::Cli
|
|
3
|
+
desc 'Tracks the files of a folder in a CSV'
|
|
4
|
+
|
|
5
|
+
callback do |_session, options, _usecase|
|
|
6
|
+
if (folder = SCR.get_file(cli_name, required: true))
|
|
7
|
+
options.deep_merge!(input: {folder: folder})
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
add_option("-s3-path", "Relative subpath from the S3 uploads folder.") do |options|
|
|
12
|
+
path = SCR.get_arg("-s3-path", with_param: true)
|
|
13
|
+
options.deep_merge!(output: {s3_path: path})
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|